diff --git a/httpserver.cpp b/httpserver.cpp new file mode 100644 index 0000000..2150fa9 --- /dev/null +++ b/httpserver.cpp @@ -0,0 +1,139 @@ +#include "httpserver.hpp" + +HTTPServer::HTTPServer(unsigned short port, size_t num_threads=1) : endpoint(ip::tcp::v4(), port), + acceptor(m_io_service, endpoint), num_threads(num_threads) {} + +void HTTPServer::start() { + accept(); + + //If num_threads>1, start m_io_service.run() in (num_threads-1) threads for thread-pooling + for(size_t c=1;c socket(new ip::tcp::socket(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); + } + }); +} + +void HTTPServer::process_request_and_respond(shared_ptr socket) { + //Create new read_buffer for async_read_until() + //Shared_ptr is used to pass temporary objects to the asynchronous functions + shared_ptr read_buffer(new boost::asio::streambuf); + + async_read_until(*socket, *read_buffer, "\r\n\r\n", + [this, socket, read_buffer](const boost::system::error_code& ec, size_t bytes_transferred) { + if(!ec) { + //read_buffer->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 + istream stream(read_buffer.get()); + + shared_ptr request(new Request()); + *request=parse_request(stream); + + size_t num_additional_bytes=total-bytes_transferred; + + //If content, read that as well + if(request->header.count("Content-Length")>0) { + async_read(*socket, *read_buffer, transfer_exactly(stoull(request->header["Content-Length"])-num_additional_bytes), + [this, socket, read_buffer, request](const boost::system::error_code& ec, size_t bytes_transferred) { + if(!ec) { + //Store pointer to read_buffer as istream object + request->content=shared_ptr(new istream(read_buffer.get())); + + respond(socket, request); + } + }); + } + else { + respond(socket, request); + } + } + }); +} + +Request HTTPServer::parse_request(istream& stream) { + Request request; + + regex e("^([^ ]*) ([^ ]*) HTTP/([^ ]*)$"); + + smatch sm; + + //First parse request method, path, and HTTP-version from the first line + string line; + getline(stream, line); + line.pop_back(); + if(regex_match(line, sm, e)) { + request.method=sm[1]; + request.path=sm[2]; + request.http_version=sm[3]; + + bool matched; + e="^([^:]*): ?(.*)$"; + //Parse the rest of the header + do { + getline(stream, line); + line.pop_back(); + matched=regex_match(line, sm, e); + if(matched) { + request.header[sm[1]]=sm[2]; + } + + } while(matched==true); + } + + return request; +} + +void HTTPServer::respond(shared_ptr socket, shared_ptr request) { + //Find path- and method-match, and generate response + for(auto& res: resources) { + regex e(res.first); + smatch sm_res; + if(regex_match(request->path, sm_res, e)) { + for(auto& res_path: res.second) { + e=res_path.first; + smatch sm_path; + if(regex_match(request->method, sm_path, e)) { + shared_ptr write_buffer(new boost::asio::streambuf); + ostream response(write_buffer.get()); + res_path.second(response, *request, sm_res); + + //Capture write_buffer in lambda so it is not destroyed before async_write is finished + async_write(*socket, *write_buffer, [this, socket, request, write_buffer](const boost::system::error_code& ec, size_t bytes_transferred) { + //HTTP persistent connection (HTTP 1.1): + if(!ec && stof(request->http_version)>1.05) + process_request_and_respond(socket); + }); + return; + } + } + } + } +} \ No newline at end of file diff --git a/httpserver.hpp b/httpserver.hpp new file mode 100644 index 0000000..9607ebe --- /dev/null +++ b/httpserver.hpp @@ -0,0 +1,46 @@ +#ifndef HTTPSERVER_HPP +#define HTTPSERVER_HPP + +#include + +#include +#include +#include + +using namespace std; +using namespace boost::asio; + +struct Request { + string method, path, http_version; + + shared_ptr content; + + unordered_map header; +}; + +class HTTPServer { +public: + unordered_map > > resources; + + HTTPServer(unsigned short, size_t); + + void start(); + +private: + io_service m_io_service; + ip::tcp::endpoint endpoint; + ip::tcp::acceptor acceptor; + size_t num_threads; + vector threads; + + void accept(); + + void process_request_and_respond(shared_ptr socket); + + Request parse_request(istream& stream); + + void respond(shared_ptr socket, shared_ptr request); +}; + +#endif /* HTTPSERVER_HPP */ + diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..19dff6a --- /dev/null +++ b/main.cpp @@ -0,0 +1,56 @@ +#include "httpserver.hpp" + +//Added for the json-example: +#include +#include + +using namespace std; +//Added for the json-example: +using namespace boost::property_tree; + +int main() { + //HTTP-server at port 8080 using 4 threads + HTTPServer httpserver(8080, 1); + + //Add resources using regular expressions for path and method + //POST-example for the path /string, responds the posted string + httpserver.resources["^/string/?$"]["^POST$"]=[](ostream& response, const Request& request, const smatch& path_match) { + //Retrieve string from istream (*request.content) + stringstream ss; + *request.content >> ss.rdbuf(); + string content=move(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 correct, or if firstName or lastName is missing + httpserver.resources["^/json/?$"]["^POST$"]=[](ostream& response, const Request& request, const smatch& path_match) { + ptree pt; + try { + 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 / + httpserver.resources["^/$"]["^GET$"]=[](ostream& response, const Request& request, const smatch& path_match) { + response << "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nRoot resource"; + }; + + //GET-example for the path /test, responds with the matched string in path (test) + httpserver.resources["^/(test)/?$"]["^GET$"]=[](ostream& response, const Request& request, const smatch& path_match) { + response << "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n" << path_match[1]; + }; + + //Start HTTP-server + httpserver.start(); + + return 0; +} \ No newline at end of file