diff --git a/README.md b/README.md index bb6f7ac..8fbbe50 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Simple-Web-Server ================= -A very simple, fast, multithreaded, platform independent HTTP and HTTPS server library implemented using C++11 and Boost.Asio. Created to be an easy way to make REST resources available from C++ applications. +A very simple, fast, multithreaded, platform independent HTTP and HTTPS server and client library implemented using C++11 and Boost.Asio. Created to be an easy way to make REST resources available from C++ applications. See also https://github.com/eidheim/Simple-WebSocket-Server for an easy way to make WebSocket/WebSocket Secure endpoints in C++. @@ -11,12 +11,13 @@ See also https://github.com/eidheim/Simple-WebSocket-Server for an easy way to m * Platform independent * HTTPS support * HTTP persistent connection (for HTTP/1.1) +* Client supports chunked transfer encoding * Timeouts, if any of Server::timeout_request and Server::timeout_content are >0 (default: Server::timeout_request=5 seconds, and Server::timeout_content=300 seconds) * Simple way to add REST resources using regex for path, and anonymous functions ###Usage -See main_http.cpp or main_https.cpp for example usage. +See http_examples.cpp or https_examples.cpp for example usage. See particularly the JSON-POST (using Boost.PropertyTree) and the GET /match/[number] examples, which are most relevant. @@ -32,19 +33,19 @@ Compile with a C++11 compiler supporting regex (for instance g++ 4.9): #### HTTP -g++ -O3 -std=c++11 -lboost_system main_http.cpp -o http_server +g++ -O3 -std=c++11 -lboost_system http_examples.cpp -o http_examples -Then to run the server: ./http_server +Then to run the server and client: ./http_examples -Finally, direct your favorite browser to for instance http://localhost:8080/ +Also, direct your favorite browser to for instance http://localhost:8080/ #### HTTPS -g++ -O3 -std=c++11 -lboost_system -lssl -lcrypto main_https.cpp -o https_server +g++ -O3 -std=c++11 -lboost_system -lssl -lcrypto https_examples -o https_examples Before running the server, an RSA private key (server.key) and an SSL certificate (server.crt) must be created. Follow, for instance, the instructions given here (for a self-signed certificate): http://www.akadia.com/services/ssh_test_certificate.html -Then to run the server: ./https_server +Then to run the server: ./https_examples -Finally, direct your favorite browser to for instance https://localhost:8080/ +Also, direct your favorite browser to for instance https://localhost:8080/ diff --git a/main_http.cpp b/main_http.cpp deleted file mode 100644 index 466710f..0000000 --- a/main_http.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include "server_http.hpp" - -//Added for the json-example: -#include -#include - -#include - -using namespace std; -using namespace SimpleWeb; -//Added for the json-example: -using namespace boost::property_tree; - -int main() { - //HTTP-server at port 8080 using 4 threads - Server server(8080, 4); - - //Add resources using regular expression for path, a method-string, and an anonymous function - //POST-example for the path /string, responds the posted string - server.resource["^/string/?$"]["POST"]=[](ostream& response, Request& request) { - //Retrieve string from istream (*request.content) - stringstream ss; - *request.content >> ss.rdbuf(); - string content=ss.str(); - - response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" << 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"]=[](ostream& response, Request& request) { - try { - ptree pt; - read_json(*request.content, pt); - - string name=pt.get("firstName")+" "+pt.get("lastName"); - - response << "HTTP/1.1 200 OK\r\nContent-Length: " << name.length() << "\r\n\r\n" << name; - } - catch(exception& e) { - response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << strlen(e.what()) << "\r\n\r\n" << e.what(); - } - }; - - //GET-example for the path /info - //Responds with request-information - server.resource["^/info/?$"]["GET"]=[](ostream& response, Request& request) { - stringstream content_stream; - content_stream << "

Request:

"; - content_stream << request.method << " " << request.path << " HTTP/" << request.http_version << "
"; - for(auto& header: request.header) { - content_stream << header.first << ": " << header.second << "
"; - } - - //find length of content_stream (length received using content_stream.tellp()) - 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(); - }; - - //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"]=[](ostream& response, Request& request) { - string number=request.path_match[1]; - response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" << number; - }; - - //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"]=[](ostream& response, Request& request) { - string filename="web/"; - - string path=request.path_match[1]; - - //Remove all but the last '.' (so we can't leave the web-directory) - size_t last_pos=path.rfind("."); - size_t current_pos=0; - size_t pos; - while((pos=path.find('.', current_pos))!=string::npos && pos!=last_pos) { - current_pos=pos; - path.erase(pos, 1); - last_pos--; - } - - filename+=path; - ifstream ifs; - //A simple platform-independent file-or-directory check do not exist, but this works in most of the cases: - if(filename.find('.')==string::npos) { - if(filename[filename.length()-1]!='/') - filename+='/'; - filename+="index.html"; - } - ifs.open(filename, ifstream::in); - - if(ifs) { - ifs.seekg(0, ios::end); - size_t length=ifs.tellg(); - - ifs.seekg(0, ios::beg); - - //The file-content is copied to the response-stream. Should not be used for very large files. - response << "HTTP/1.1 200 OK\r\nContent-Length: " << length << "\r\n\r\n" << ifs.rdbuf(); - - ifs.close(); - } - else { - string content="Could not open file "+filename; - response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << content.length() << "\r\n\r\n" << content; - } - }; - - //Start HTTP-server - server.start(); - - return 0; -} diff --git a/main_https.cpp b/main_https.cpp deleted file mode 100644 index 18676a8..0000000 --- a/main_https.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include "server_https.hpp" - -//Added for the json-example: -#include -#include - -#include - -using namespace std; -using namespace SimpleWeb; -//Added for the json-example: -using namespace boost::property_tree; - -int main() { - //HTTPS-server at port 8080 using 4 threads - Server server(8080, 4, "server.crt", "server.key"); - - //Add resources using regular expression for path, a method-string, and an anonymous function - //POST-example for the path /string, responds the posted string - server.resource["^/string/?$"]["POST"]=[](ostream& response, Request& request) { - //Retrieve string from istream (*request.content) - stringstream ss; - *request.content >> ss.rdbuf(); - string content=ss.str(); - - response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" << 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"]=[](ostream& response, Request& request) { - try { - ptree pt; - read_json(*request.content, pt); - - string name=pt.get("firstName")+" "+pt.get("lastName"); - - response << "HTTP/1.1 200 OK\r\nContent-Length: " << name.length() << "\r\n\r\n" << name; - } - catch(exception& e) { - response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << strlen(e.what()) << "\r\n\r\n" << e.what(); - } - }; - - //GET-example for the path /info - //Responds with request-information - server.resource["^/info/?$"]["GET"]=[](ostream& response, Request& request) { - stringstream content_stream; - content_stream << "

Request:

"; - content_stream << request.method << " " << request.path << " HTTP/" << request.http_version << "
"; - for(auto& header: request.header) { - content_stream << header.first << ": " << header.second << "
"; - } - - //find length of content_stream (length received using content_stream.tellp()) - 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(); - }; - - //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"]=[](ostream& response, Request& request) { - string number=request.path_match[1]; - response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" << number; - }; - - //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"]=[](ostream& response, Request& request) { - string filename="web/"; - - string path=request.path_match[1]; - - //Remove all but the last '.' (so we can't leave the web-directory) - size_t last_pos=path.rfind("."); - size_t current_pos=0; - size_t pos; - while((pos=path.find('.', current_pos))!=string::npos && pos!=last_pos) { - current_pos=pos; - path.erase(pos, 1); - last_pos--; - } - - filename+=path; - ifstream ifs; - //A simple platform-independent file-or-directory check do not exist, but this works in most of the cases: - if(filename.find('.')==string::npos) { - if(filename[filename.length()-1]!='/') - filename+='/'; - filename+="index.html"; - } - ifs.open(filename, ifstream::in); - - if(ifs) { - ifs.seekg(0, ios::end); - size_t length=ifs.tellg(); - - ifs.seekg(0, ios::beg); - - //The file-content is copied to the response-stream. Should not be used for very large files. - response << "HTTP/1.1 200 OK\r\nContent-Length: " << length << "\r\n\r\n" << ifs.rdbuf(); - - ifs.close(); - } - else { - string content="Could not open file "+filename; - response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << content.length() << "\r\n\r\n" << content; - } - }; - - //Start HTTPS-server - server.start(); - - return 0; -} diff --git a/server_http.hpp b/server_http.hpp index 0af2e8a..53c3b54 100644 --- a/server_http.hpp +++ b/server_http.hpp @@ -8,21 +8,29 @@ #include namespace SimpleWeb { - struct Request { - std::string method, path, http_version; - - std::shared_ptr content; - - std::unordered_map header; - - std::smatch path_match; - }; - - typedef std::map > > resource_type; - template class ServerBase { public: + class Request { + friend class ServerBase; + public: + std::string method, path, http_version; + + std::istream content; + + std::unordered_map header; + + std::smatch path_match; + + private: + Request(): content(&streambuf) {} + + boost::asio::streambuf streambuf; + }; + + typedef std::map::Request>)> > > resource_type; + resource_type resource; resource_type default_resource; @@ -67,7 +75,7 @@ namespace SimpleWeb { //All resources with default_resource at the end of vector //Created in start() - std::vector all_resources; + std::vector all_resources; ServerBase(unsigned short port, size_t num_threads, size_t timeout_request, size_t timeout_send_or_receive) : endpoint(boost::asio::ip::tcp::v4(), port), acceptor(m_io_service, endpoint), num_threads(num_threads), @@ -77,34 +85,28 @@ namespace SimpleWeb { virtual std::shared_ptr set_timeout_on_socket(std::shared_ptr socket, size_t seconds)=0; - void process_request_and_respond(std::shared_ptr socket) { - //Create new read_buffer for async_read_until() - //Shared_ptr is used to pass temporary objects to the asynchronous functions - std::shared_ptr read_buffer(new boost::asio::streambuf); + void read_request_and_content(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()); //Set timeout on the following boost::asio::async-read or write function std::shared_ptr timer; if(timeout_request>0) timer=set_timeout_on_socket(socket, timeout_request); - boost::asio::async_read_until(*socket, *read_buffer, "\r\n\r\n", - [this, socket, read_buffer, timer](const boost::system::error_code& ec, size_t bytes_transferred) { + boost::asio::async_read_until(*socket, request->streambuf, "\r\n\r\n", + [this, socket, request, timer](const boost::system::error_code& ec, size_t bytes_transferred) { if(timeout_request>0) timer->cancel(); if(!ec) { - //read_buffer->size() is not necessarily the same as bytes_transferred, from Boost-docs: + //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 - //read_buffer (maybe some bytes of the content) is appended to in the async_read-function below (for retrieving content). - size_t total=read_buffer->size(); - - //Convert to istream to extract string-lines - std::istream stream(read_buffer.get()); + //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; - std::shared_ptr request(new Request()); - *request=parse_request(stream); - - size_t num_additional_bytes=total-bytes_transferred; + parse_request(request, request->content); //If content, read that as well if(request->header.count("Content-Length")>0) { @@ -113,30 +115,24 @@ namespace SimpleWeb { if(timeout_content>0) timer=set_timeout_on_socket(socket, timeout_content); - boost::asio::async_read(*socket, *read_buffer, + boost::asio::async_read(*socket, request->streambuf, boost::asio::transfer_exactly(stoull(request->header["Content-Length"])-num_additional_bytes), - [this, socket, read_buffer, request, timer] + [this, socket, request, timer] (const boost::system::error_code& ec, size_t bytes_transferred) { if(timeout_content>0) timer->cancel(); - if(!ec) { - //Store pointer to read_buffer as istream object - request->content=std::shared_ptr(new std::istream(read_buffer.get())); - - respond(socket, request); - } + if(!ec) + write_response(socket, request); }); } else { - respond(socket, request); + write_response(socket, request); } } }); } - Request parse_request(std::istream& stream) const { - Request request; - + void parse_request(std::shared_ptr request, std::istream& stream) const { std::regex e("^([^ ]*) ([^ ]*) HTTP/([^ ]*)$"); std::smatch sm; @@ -146,9 +142,9 @@ namespace SimpleWeb { getline(stream, line); line.pop_back(); if(std::regex_match(line, sm, e)) { - request.method=sm[1]; - request.path=sm[2]; - request.http_version=sm[3]; + request->method=sm[1]; + request->path=sm[2]; + request->http_version=sm[3]; bool matched; e="^([^:]*): ?(.*)$"; @@ -158,16 +154,14 @@ namespace SimpleWeb { line.pop_back(); matched=std::regex_match(line, sm, e); if(matched) { - request.header[sm[1]]=sm[2]; + request->header[sm[1]]=sm[2]; } } while(matched==true); } - - return request; } - void respond(std::shared_ptr socket, std::shared_ptr request) { + void write_response(std::shared_ptr socket, std::shared_ptr request) { //Find path- and method-match, and generate response for(auto res_it: all_resources) { std::regex e(res_it->first); @@ -178,7 +172,7 @@ namespace SimpleWeb { std::shared_ptr write_buffer(new boost::asio::streambuf); std::ostream response(write_buffer.get()); - res_it->second[request->method](response, *request); + res_it->second[request->method](response, request); //Set timeout on the following boost::asio::async-read or write function std::shared_ptr timer; @@ -193,7 +187,7 @@ namespace SimpleWeb { timer->cancel(); //HTTP persistent connection (HTTP 1.1): if(!ec && stof(request->http_version)>1.05) - process_request_and_respond(socket); + read_request_and_content(socket); }); return; } @@ -218,13 +212,13 @@ namespace SimpleWeb { //Create new socket for this connection //Shared_ptr is used to pass temporary objects to the asynchronous functions std::shared_ptr socket(new HTTP(m_io_service)); - + acceptor.async_accept(*socket, [this, socket](const boost::system::error_code& ec) { //Immediately start accepting a new connection accept(); if(!ec) { - process_request_and_respond(socket); + read_request_and_content(socket); } }); } diff --git a/server_https.hpp b/server_https.hpp index f2a4efe..faf2fcb 100644 --- a/server_https.hpp +++ b/server_https.hpp @@ -40,7 +40,7 @@ namespace SimpleWeb { if(timeout_request>0) timer->cancel(); if(!ec) - process_request_and_respond(socket); + read_request_and_content(socket); }); } });