diff --git a/scripts/src/lib.lua b/scripts/src/lib.lua index 51b92d098d2..b89b58655d7 100644 --- a/scripts/src/lib.lua +++ b/scripts/src/lib.lua @@ -100,3 +100,35 @@ project "utils" MAME_DIR .. "src/lib/util/zippath.cpp", MAME_DIR .. "src/lib/util/zippath.h", } + + +project "http" + uuid "d7930d44-21d1-4c5d-b6af-582c141cd23a" + kind (LIBTYPE) + + addprojectflags() + + includedirs { + MAME_DIR .. "3rdparty", + MAME_DIR .. "3rdparty/asio/include", + MAME_DIR .. "src/osd", + } + + files { + MAME_DIR .. "src/lib/http/connection.cpp", + MAME_DIR .. "src/lib/http/connection.hpp", + MAME_DIR .. "src/lib/http/connection_manager.cpp", + MAME_DIR .. "src/lib/http/connection_manager.hpp", + MAME_DIR .. "src/lib/http/header.hpp", + MAME_DIR .. "src/lib/http/mime_types.cpp", + MAME_DIR .. "src/lib/http/mime_types.hpp", + MAME_DIR .. "src/lib/http/reply.cpp", + MAME_DIR .. "src/lib/http/reply.hpp", + MAME_DIR .. "src/lib/http/request.hpp", + MAME_DIR .. "src/lib/http/request_handler.cpp", + MAME_DIR .. "src/lib/http/request_handler.hpp", + MAME_DIR .. "src/lib/http/request_parser.cpp", + MAME_DIR .. "src/lib/http/request_parser.hpp", + MAME_DIR .. "src/lib/http/server.cpp", + MAME_DIR .. "src/lib/http/server.hpp", + } diff --git a/src/lib/http/connection.cpp b/src/lib/http/connection.cpp new file mode 100644 index 00000000000..bb1c07cfa06 --- /dev/null +++ b/src/lib/http/connection.cpp @@ -0,0 +1,87 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#include "connection.hpp" +#include +#include +#include "connection_manager.hpp" +#include "request_handler.hpp" + +namespace http { +namespace server { + +connection::connection(asio::ip::tcp::socket socket, + connection_manager& manager, request_handler& handler) + : m_socket(std::move(socket)), + m_connection_manager(manager), + m_request_handler(handler) +{ +} + +void connection::start() +{ + do_read(); +} + +void connection::stop() +{ + m_socket.close(); +} + +void connection::do_read() +{ + auto self(shared_from_this()); + m_socket.async_read_some(asio::buffer(m_buffer), + [this, self](std::error_code ec, std::size_t bytes_transferred) + { + if (!ec) + { + request_parser::result_type result; + std::tie(result, std::ignore) = m_request_parser.parse( + m_request, m_buffer.data(), m_buffer.data() + bytes_transferred); + + if (result == request_parser::good) + { + m_request_handler.handle_request(m_request, m_reply); + do_write(); + } + else if (result == request_parser::bad) + { + m_reply = reply::stock_reply(reply::bad_request); + do_write(); + } + else + { + do_read(); + } + } + else if (ec != asio::error::operation_aborted) + { + m_connection_manager.stop(shared_from_this()); + } + }); +} + +void connection::do_write() +{ + auto self(shared_from_this()); + asio::async_write(m_socket, m_reply.to_buffers(), + [this, self](std::error_code ec, std::size_t) + { + if (!ec) + { + // Initiate graceful connection closure. + asio::error_code ignored_ec; + m_socket.shutdown(asio::ip::tcp::socket::shutdown_both, + ignored_ec); + } + + if (ec != asio::error::operation_aborted) + { + m_connection_manager.stop(shared_from_this()); + } + }); +} + +} // namespace server +} // namespace http diff --git a/src/lib/http/connection.hpp b/src/lib/http/connection.hpp new file mode 100644 index 00000000000..6ad1768242e --- /dev/null +++ b/src/lib/http/connection.hpp @@ -0,0 +1,72 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#ifndef HTTP_CONNECTION_HPP +#define HTTP_CONNECTION_HPP + +#include +#include +#include "asio.h" +#include "reply.hpp" +#include "request.hpp" +#include "request_handler.hpp" +#include "request_parser.hpp" + +namespace http { +namespace server { + +class connection_manager; + +/// Represents a single connection from a client. +class connection + : public std::enable_shared_from_this +{ +public: + connection(const connection&) = delete; + connection& operator=(const connection&) = delete; + + /// Construct a connection with the given socket. + explicit connection(asio::ip::tcp::socket socket, + connection_manager& manager, request_handler& handler); + + /// Start the first asynchronous operation for the connection. + void start(); + + /// Stop all asynchronous operations associated with the connection. + void stop(); + +private: + /// Perform an asynchronous read operation. + void do_read(); + + /// Perform an asynchronous write operation. + void do_write(); + + /// Socket for the connection. + asio::ip::tcp::socket m_socket; + + /// The manager for this connection. + connection_manager& m_connection_manager; + + /// The handler used to process the incoming request. + request_handler& m_request_handler; + + /// Buffer for incoming data. + std::array m_buffer; + + /// The incoming request. + request m_request; + + /// The parser for the incoming request. + request_parser m_request_parser; + + /// The reply to be sent back to the client. + reply m_reply; +}; + +typedef std::shared_ptr connection_ptr; + +} // namespace server +} // namespace http + +#endif // HTTP_CONNECTION_HPP diff --git a/src/lib/http/connection_manager.cpp b/src/lib/http/connection_manager.cpp new file mode 100644 index 00000000000..97464bf3639 --- /dev/null +++ b/src/lib/http/connection_manager.cpp @@ -0,0 +1,33 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#include "connection_manager.hpp" + +namespace http { +namespace server { + +connection_manager::connection_manager() +{ +} + +void connection_manager::start(connection_ptr c) +{ + m_connections.insert(c); + c->start(); +} + +void connection_manager::stop(connection_ptr c) +{ + m_connections.erase(c); + c->stop(); +} + +void connection_manager::stop_all() +{ + for (auto c: m_connections) + c->stop(); + m_connections.clear(); +} + +} // namespace server +} // namespace http diff --git a/src/lib/http/connection_manager.hpp b/src/lib/http/connection_manager.hpp new file mode 100644 index 00000000000..ad215d05c69 --- /dev/null +++ b/src/lib/http/connection_manager.hpp @@ -0,0 +1,41 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#ifndef HTTP_CONNECTION_MANAGER_HPP +#define HTTP_CONNECTION_MANAGER_HPP + +#include +#include "connection.hpp" + +namespace http { +namespace server { + +/// Manages open connections so that they may be cleanly stopped when the server +/// needs to shut down. +class connection_manager +{ +public: + connection_manager(const connection_manager&) = delete; + connection_manager& operator=(const connection_manager&) = delete; + + /// Construct a connection manager. + connection_manager(); + + /// Add the specified connection to the manager and start it. + void start(connection_ptr c); + + /// Stop the specified connection. + void stop(connection_ptr c); + + /// Stop all connections. + void stop_all(); + +private: + /// The managed connections. + std::set m_connections; +}; + +} // namespace server +} // namespace http + +#endif // HTTP_CONNECTION_MANAGER_HPP diff --git a/src/lib/http/header.hpp b/src/lib/http/header.hpp new file mode 100644 index 00000000000..7cad96f2439 --- /dev/null +++ b/src/lib/http/header.hpp @@ -0,0 +1,21 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#ifndef HTTP_HEADER_HPP +#define HTTP_HEADER_HPP + +#include + +namespace http { +namespace server { + +struct header +{ + std::string name; + std::string value; +}; + +} // namespace server +} // namespace http + +#endif // HTTP_HEADER_HPP diff --git a/src/lib/http/mime_types.cpp b/src/lib/http/mime_types.cpp new file mode 100644 index 00000000000..3ca649dfb80 --- /dev/null +++ b/src/lib/http/mime_types.cpp @@ -0,0 +1,106 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#include "mime_types.hpp" + +namespace http { +namespace server { +namespace mime_types { + +struct mapping +{ + const char* extension; + const char* mime_type; +} mappings[] = +{ + { "aac", "audio/aac" }, + { "aat", "application/font-sfnt" }, + { "aif", "audio/x-aif" }, + { "arj", "application/x-arj-compressed" }, + { "asf", "video/x-ms-asf" }, + { "avi", "video/x-msvideo" }, + { "bmp", "image/bmp" }, + { "cff", "application/font-sfnt" }, + { "css", "text/css" }, + { "csv", "text/csv" }, + { "doc", "application/msword" }, + { "eps", "application/postscript" }, + { "exe", "application/octet-stream" }, + { "gif", "image/gif" }, + { "gz", "application/x-gunzip" }, + { "htm", "text/html" }, + { "html", "text/html" }, + { "ico", "image/x-icon" }, + { "ief", "image/ief" }, + { "jpeg", "image/jpeg" }, + { "jpg", "image/jpeg" }, + { "jpm", "image/jpm" }, + { "jpx", "image/jpx" }, + { "js", "application/javascript" }, + { "json", "application/json" }, + { "m3u", "audio/x-mpegurl" }, + { "m4v", "video/x-m4v" }, + { "mid", "audio/x-midi" }, + { "mov", "video/quicktime" }, + { "mp3", "audio/mpeg" }, + { "mp4", "video/mp4" }, + { "mpeg", "video/mpeg" }, + { "mpg", "video/mpeg" }, + { "oga", "audio/ogg" }, + { "ogg", "audio/ogg" }, + { "ogv", "video/ogg" }, + { "otf", "application/font-sfnt" }, + { "pct", "image/x-pct" }, + { "pdf", "application/pdf" }, + { "pfr", "application/font-tdpfr" }, + { "pict", "image/pict" }, + { "png", "image/png" }, + { "ppt", "application/x-mspowerpoint" }, + { "ps", "application/postscript" }, + { "qt", "video/quicktime" }, + { "ra", "audio/x-pn-realaudio" }, + { "ram", "audio/x-pn-realaudio" }, + { "rar", "application/x-arj-compressed" }, + { "rgb", "image/x-rgb" }, + { "rtf", "application/rtf" }, + { "sgm", "text/sgml" }, + { "shtm", "text/html" }, + { "shtml", "text/html" }, + { "sil", "application/font-sfnt" }, + { "svg", "image/svg+xml" }, + { "swf", "application/x-shockwave-flash" }, + { "tar", "application/x-tar" }, + { "tgz", "application/x-tar-gz" }, + { "tif", "image/tiff" }, + { "tiff", "image/tiff" }, + { "torrent", "application/x-bittorrent" }, + { "ttf", "application/font-sfnt" }, + { "txt", "text/plain" }, + { "wav", "audio/x-wav" }, + { "webm", "video/webm" }, + { "woff", "application/font-woff" }, + { "wrl", "model/vrml" }, + { "xhtml", "application/xhtml+xml" }, + { "xls", "application/x-msexcel" }, + { "xml", "text/xml" }, + { "xsl", "application/xml" }, + { "xslt", "application/xml" }, + { "zip", "application/x-zip-compressed" } +}; + +std::string extension_to_type(const std::string& extension) +{ + for (mapping m: mappings) + { + if (m.extension == extension) + { + return m.mime_type; + } + } + + return "text/plain"; +} + +} // namespace mime_types +} // namespace server +} // namespace http diff --git a/src/lib/http/mime_types.hpp b/src/lib/http/mime_types.hpp new file mode 100644 index 00000000000..c0314f22dd2 --- /dev/null +++ b/src/lib/http/mime_types.hpp @@ -0,0 +1,20 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#ifndef HTTP_MIME_TYPES_HPP +#define HTTP_MIME_TYPES_HPP + +#include + +namespace http { +namespace server { +namespace mime_types { + +/// Convert a file extension into a MIME type. +std::string extension_to_type(const std::string& extension); + +} // namespace mime_types +} // namespace server +} // namespace http + +#endif // HTTP_MIME_TYPES_HPP diff --git a/src/lib/http/reply.cpp b/src/lib/http/reply.cpp new file mode 100644 index 00000000000..0997865f336 --- /dev/null +++ b/src/lib/http/reply.cpp @@ -0,0 +1,248 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#include "reply.hpp" +#include + +namespace http { +namespace server { + +namespace status_strings { + +const std::string ok = + "HTTP/1.0 200 OK\r\n"; +const std::string created = + "HTTP/1.0 201 Created\r\n"; +const std::string accepted = + "HTTP/1.0 202 Accepted\r\n"; +const std::string no_content = + "HTTP/1.0 204 No Content\r\n"; +const std::string multiple_choices = + "HTTP/1.0 300 Multiple Choices\r\n"; +const std::string moved_permanently = + "HTTP/1.0 301 Moved Permanently\r\n"; +const std::string moved_temporarily = + "HTTP/1.0 302 Moved Temporarily\r\n"; +const std::string not_modified = + "HTTP/1.0 304 Not Modified\r\n"; +const std::string bad_request = + "HTTP/1.0 400 Bad Request\r\n"; +const std::string unauthorized = + "HTTP/1.0 401 Unauthorized\r\n"; +const std::string forbidden = + "HTTP/1.0 403 Forbidden\r\n"; +const std::string not_found = + "HTTP/1.0 404 Not Found\r\n"; +const std::string internal_server_error = + "HTTP/1.0 500 Internal Server Error\r\n"; +const std::string not_implemented = + "HTTP/1.0 501 Not Implemented\r\n"; +const std::string bad_gateway = + "HTTP/1.0 502 Bad Gateway\r\n"; +const std::string service_unavailable = + "HTTP/1.0 503 Service Unavailable\r\n"; + +asio::const_buffer to_buffer(reply::status_type status) +{ + switch (status) + { + case reply::ok: + return asio::buffer(ok); + case reply::created: + return asio::buffer(created); + case reply::accepted: + return asio::buffer(accepted); + case reply::no_content: + return asio::buffer(no_content); + case reply::multiple_choices: + return asio::buffer(multiple_choices); + case reply::moved_permanently: + return asio::buffer(moved_permanently); + case reply::moved_temporarily: + return asio::buffer(moved_temporarily); + case reply::not_modified: + return asio::buffer(not_modified); + case reply::bad_request: + return asio::buffer(bad_request); + case reply::unauthorized: + return asio::buffer(unauthorized); + case reply::forbidden: + return asio::buffer(forbidden); + case reply::not_found: + return asio::buffer(not_found); + case reply::internal_server_error: + return asio::buffer(internal_server_error); + case reply::not_implemented: + return asio::buffer(not_implemented); + case reply::bad_gateway: + return asio::buffer(bad_gateway); + case reply::service_unavailable: + return asio::buffer(service_unavailable); + default: + return asio::buffer(internal_server_error); + } +} + +} // namespace status_strings + +namespace misc_strings { + +const char name_value_separator[] = { ':', ' ' }; +const char crlf[] = { '\r', '\n' }; + +} // namespace misc_strings + +std::vector reply::to_buffers() +{ + std::vector buffers; + buffers.push_back(status_strings::to_buffer(status)); + for (std::size_t i = 0; i < headers.size(); ++i) + { + header& h = headers[i]; + buffers.push_back(asio::buffer(h.name)); + buffers.push_back(asio::buffer(misc_strings::name_value_separator)); + buffers.push_back(asio::buffer(h.value)); + buffers.push_back(asio::buffer(misc_strings::crlf)); + } + buffers.push_back(asio::buffer(misc_strings::crlf)); + buffers.push_back(asio::buffer(content)); + return buffers; +} + +namespace stock_replies { + +const char ok[] = ""; +const char created[] = + "" + "Created" + "

201 Created

" + ""; +const char accepted[] = + "" + "Accepted" + "

202 Accepted

" + ""; +const char no_content[] = + "" + "No Content" + "

204 Content

" + ""; +const char multiple_choices[] = + "" + "Multiple Choices" + "

300 Multiple Choices

" + ""; +const char moved_permanently[] = + "" + "Moved Permanently" + "

301 Moved Permanently

" + ""; +const char moved_temporarily[] = + "" + "Moved Temporarily" + "

302 Moved Temporarily

" + ""; +const char not_modified[] = + "" + "Not Modified" + "

304 Not Modified

" + ""; +const char bad_request[] = + "" + "Bad Request" + "

400 Bad Request

" + ""; +const char unauthorized[] = + "" + "Unauthorized" + "

401 Unauthorized

" + ""; +const char forbidden[] = + "" + "Forbidden" + "

403 Forbidden

" + ""; +const char not_found[] = + "" + "Not Found" + "

404 Not Found

" + ""; +const char internal_server_error[] = + "" + "Internal Server Error" + "

500 Internal Server Error

" + ""; +const char not_implemented[] = + "" + "Not Implemented" + "

501 Not Implemented

" + ""; +const char bad_gateway[] = + "" + "Bad Gateway" + "

502 Bad Gateway

" + ""; +const char service_unavailable[] = + "" + "Service Unavailable" + "

503 Service Unavailable

" + ""; + +std::string to_string(reply::status_type status) +{ + switch (status) + { + case reply::ok: + return ok; + case reply::created: + return created; + case reply::accepted: + return accepted; + case reply::no_content: + return no_content; + case reply::multiple_choices: + return multiple_choices; + case reply::moved_permanently: + return moved_permanently; + case reply::moved_temporarily: + return moved_temporarily; + case reply::not_modified: + return not_modified; + case reply::bad_request: + return bad_request; + case reply::unauthorized: + return unauthorized; + case reply::forbidden: + return forbidden; + case reply::not_found: + return not_found; + case reply::internal_server_error: + return internal_server_error; + case reply::not_implemented: + return not_implemented; + case reply::bad_gateway: + return bad_gateway; + case reply::service_unavailable: + return service_unavailable; + default: + return internal_server_error; + } +} + +} // namespace stock_replies + +reply reply::stock_reply(reply::status_type status) +{ + reply rep; + rep.status = status; + rep.content = stock_replies::to_string(status); + rep.headers.resize(2); + rep.headers[0].name = "Content-Length"; + rep.headers[0].value = std::to_string(rep.content.size()); + rep.headers[1].name = "Content-Type"; + rep.headers[1].value = "text/html"; + return rep; +} + +} // namespace server +} // namespace http diff --git a/src/lib/http/reply.hpp b/src/lib/http/reply.hpp new file mode 100644 index 00000000000..7e05752f3e0 --- /dev/null +++ b/src/lib/http/reply.hpp @@ -0,0 +1,57 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#ifndef HTTP_REPLY_HPP +#define HTTP_REPLY_HPP + +#include +#include +#include "asio.h" +#include "header.hpp" + +namespace http { +namespace server { + +/// A reply to be sent to a client. +struct reply +{ + /// The status of the reply. + enum status_type + { + ok = 200, + created = 201, + accepted = 202, + no_content = 204, + multiple_choices = 300, + moved_permanently = 301, + moved_temporarily = 302, + not_modified = 304, + bad_request = 400, + unauthorized = 401, + forbidden = 403, + not_found = 404, + internal_server_error = 500, + not_implemented = 501, + bad_gateway = 502, + service_unavailable = 503 + } status; + + /// The headers to be included in the reply. + std::vector
headers; + + /// The content to be sent in the reply. + std::string content; + + /// Convert the reply into a vector of buffers. The buffers do not own the + /// underlying memory blocks, therefore the reply object must remain valid and + /// not be changed until the write operation has completed. + std::vector to_buffers(); + + /// Get a stock reply. + static reply stock_reply(status_type status); +}; + +} // namespace server +} // namespace http + +#endif // HTTP_REPLY_HPP diff --git a/src/lib/http/request.hpp b/src/lib/http/request.hpp new file mode 100644 index 00000000000..fc824435cb3 --- /dev/null +++ b/src/lib/http/request.hpp @@ -0,0 +1,27 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#ifndef HTTP_REQUEST_HPP +#define HTTP_REQUEST_HPP + +#include +#include +#include "header.hpp" + +namespace http { +namespace server { + +/// A request received from a client. +struct request +{ + std::string method; + std::string uri; + int http_version_major; + int http_version_minor; + std::vector
headers; +}; + +} // namespace server +} // namespace http + +#endif // HTTP_REQUEST_HPP diff --git a/src/lib/http/request_handler.cpp b/src/lib/http/request_handler.cpp new file mode 100644 index 00000000000..3bc4f86de29 --- /dev/null +++ b/src/lib/http/request_handler.cpp @@ -0,0 +1,125 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#include "request_handler.hpp" +#include +#include +#include +#include "mime_types.hpp" +#include "reply.hpp" +#include "request.hpp" + +namespace http { +namespace server { + +request_handler::request_handler(const std::string& doc_root) + : m_doc_root(doc_root) +{ +} + +bool request_handler::serve_static_content(std::string& request_path,reply& rep) const +{ + // If path ends in slash (i.e. is a directory) then add "index.html". + if (request_path[request_path.size() - 1] == '/') + { + request_path += "index.html"; + } + + std::size_t last_qmark_pos = request_path.find_last_of("?"); + if (last_qmark_pos!=std::string::npos) + request_path = request_path.substr(0, last_qmark_pos - 1); + + // Determine the file extension. + std::size_t last_slash_pos = request_path.find_last_of("/"); + std::size_t last_dot_pos = request_path.find_last_of("."); + std::string extension; + if (last_dot_pos != std::string::npos && last_dot_pos > last_slash_pos) + { + extension = request_path.substr(last_dot_pos + 1); + } + + // Open the file to send back. + std::string full_path = m_doc_root + request_path; + std::ifstream is(full_path.c_str(), std::ios::in | std::ios::binary); + if (!is) + { + return false; + } + + // Fill out the reply to be sent to the client. + rep.status = reply::ok; + char buf[512]; + while (is.read(buf, sizeof(buf)).gcount() > 0) + rep.content.append(buf, is.gcount()); + rep.headers.resize(2); + rep.headers[0].name = "Content-Length"; + rep.headers[0].value = std::to_string(rep.content.size()); + rep.headers[1].name = "Content-Type"; + rep.headers[1].value = mime_types::extension_to_type(extension); + return true; +} + +void request_handler::handle_request(const request& req, reply& rep) const +{ + // Decode url to path. + std::string request_path; + if (!url_decode(req.uri, request_path)) + { + rep = reply::stock_reply(reply::bad_request); + return; + } + + // Request path must be absolute and not contain "..". + if (request_path.empty() || request_path[0] != '/' || request_path.find("..") != std::string::npos) + { + rep = reply::stock_reply(reply::bad_request); + return; + } + + if (!serve_static_content(request_path, rep)) + { + rep = reply::stock_reply(reply::not_found); + } +} + +bool request_handler::url_decode(const std::string& in, std::string& out) +{ + out.clear(); + out.reserve(in.size()); + for (std::size_t i = 0; i < in.size(); ++i) + { + if (in[i] == '%') + { + if (i + 3 <= in.size()) + { + int value = 0; + std::istringstream is(in.substr(i + 1, 2)); + if (is >> std::hex >> value) + { + out += static_cast(value); + i += 2; + } + else + { + return false; + } + } + else + { + return false; + } + } + else if (in[i] == '+') + { + out += ' '; + } + else + { + out += in[i]; + } + } + return true; +} + +} // namespace server +} // namespace http diff --git a/src/lib/http/request_handler.hpp b/src/lib/http/request_handler.hpp new file mode 100644 index 00000000000..0554aa114c5 --- /dev/null +++ b/src/lib/http/request_handler.hpp @@ -0,0 +1,42 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#ifndef HTTP_REQUEST_HANDLER_HPP +#define HTTP_REQUEST_HANDLER_HPP + +#include + +namespace http { +namespace server { + +struct reply; +struct request; + +/// The common handler for all incoming requests. +class request_handler +{ +public: + request_handler(const request_handler&) = delete; + request_handler& operator=(const request_handler&) = delete; + + /// Construct with a directory containing files to be served. + explicit request_handler(const std::string& doc_root); + + /// Handle a request and produce a reply. + void handle_request(const request& req, reply& rep) const; + + bool serve_static_content(std::string& request_path, reply& rep) const; + +private: + /// The directory containing the files to be served. + std::string m_doc_root; + + /// Perform URL-decoding on a string. Returns false if the encoding was + /// invalid. + static bool url_decode(const std::string& in, std::string& out); +}; + +} // namespace server +} // namespace http + +#endif // HTTP_REQUEST_HANDLER_HPP diff --git a/src/lib/http/request_parser.cpp b/src/lib/http/request_parser.cpp new file mode 100644 index 00000000000..f1e69ed677d --- /dev/null +++ b/src/lib/http/request_parser.cpp @@ -0,0 +1,308 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#include "request_parser.hpp" +#include "request.hpp" + +namespace http { +namespace server { + +request_parser::request_parser() + : m_state(method_start) +{ +} + +void request_parser::reset() +{ + m_state = method_start; +} + +request_parser::result_type request_parser::consume(request& req, char input) +{ + switch (m_state) + { + case method_start: + if (!is_char(input) || is_ctl(input) || is_tspecial(input)) + { + return bad; + } + else + { + m_state = method; + req.method.push_back(input); + return indeterminate; + } + case method: + if (input == ' ') + { + m_state = uri; + return indeterminate; + } + else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) + { + return bad; + } + else + { + req.method.push_back(input); + return indeterminate; + } + case uri: + if (input == ' ') + { + m_state = http_version_h; + return indeterminate; + } + else if (is_ctl(input)) + { + return bad; + } + else + { + req.uri.push_back(input); + return indeterminate; + } + case http_version_h: + if (input == 'H') + { + m_state = http_version_t_1; + return indeterminate; + } + else + { + return bad; + } + case http_version_t_1: + if (input == 'T') + { + m_state = http_version_t_2; + return indeterminate; + } + else + { + return bad; + } + case http_version_t_2: + if (input == 'T') + { + m_state = http_version_p; + return indeterminate; + } + else + { + return bad; + } + case http_version_p: + if (input == 'P') + { + m_state = http_version_slash; + return indeterminate; + } + else + { + return bad; + } + case http_version_slash: + if (input == '/') + { + req.http_version_major = 0; + req.http_version_minor = 0; + m_state = http_version_major_start; + return indeterminate; + } + else + { + return bad; + } + case http_version_major_start: + if (is_digit(input)) + { + req.http_version_major = req.http_version_major * 10 + input - '0'; + m_state = http_version_major; + return indeterminate; + } + else + { + return bad; + } + case http_version_major: + if (input == '.') + { + m_state = http_version_minor_start; + return indeterminate; + } + else if (is_digit(input)) + { + req.http_version_major = req.http_version_major * 10 + input - '0'; + return indeterminate; + } + else + { + return bad; + } + case http_version_minor_start: + if (is_digit(input)) + { + req.http_version_minor = req.http_version_minor * 10 + input - '0'; + m_state = http_version_minor; + return indeterminate; + } + else + { + return bad; + } + case http_version_minor: + if (input == '\r') + { + m_state = expecting_newline_1; + return indeterminate; + } + else if (is_digit(input)) + { + req.http_version_minor = req.http_version_minor * 10 + input - '0'; + return indeterminate; + } + else + { + return bad; + } + case expecting_newline_1: + if (input == '\n') + { + m_state = header_line_start; + return indeterminate; + } + else + { + return bad; + } + case header_line_start: + if (input == '\r') + { + m_state = expecting_newline_3; + return indeterminate; + } + else if (!req.headers.empty() && (input == ' ' || input == '\t')) + { + m_state = header_lws; + return indeterminate; + } + else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) + { + return bad; + } + else + { + req.headers.push_back(header()); + req.headers.back().name.push_back(input); + m_state = header_name; + return indeterminate; + } + case header_lws: + if (input == '\r') + { + m_state = expecting_newline_2; + return indeterminate; + } + else if (input == ' ' || input == '\t') + { + return indeterminate; + } + else if (is_ctl(input)) + { + return bad; + } + else + { + m_state = header_value; + req.headers.back().value.push_back(input); + return indeterminate; + } + case header_name: + if (input == ':') + { + m_state = space_before_header_value; + return indeterminate; + } + else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) + { + return bad; + } + else + { + req.headers.back().name.push_back(input); + return indeterminate; + } + case space_before_header_value: + if (input == ' ') + { + m_state = header_value; + return indeterminate; + } + else + { + return bad; + } + case header_value: + if (input == '\r') + { + m_state = expecting_newline_2; + return indeterminate; + } + else if (is_ctl(input)) + { + return bad; + } + else + { + req.headers.back().value.push_back(input); + return indeterminate; + } + case expecting_newline_2: + if (input == '\n') + { + m_state = header_line_start; + return indeterminate; + } + else + { + return bad; + } + case expecting_newline_3: + return (input == '\n') ? good : bad; + default: + return bad; + } +} + +bool request_parser::is_char(int c) +{ + return c >= 0 && c <= 127; +} + +bool request_parser::is_ctl(int c) +{ + return (c >= 0 && c <= 31) || (c == 127); +} + +bool request_parser::is_tspecial(int c) +{ + switch (c) + { + case '(': case ')': case '<': case '>': case '@': + case ',': case ';': case ':': case '\\': case '"': + case '/': case '[': case ']': case '?': case '=': + case '{': case '}': case ' ': case '\t': + return true; + default: + return false; + } +} + +bool request_parser::is_digit(int c) +{ + return c >= '0' && c <= '9'; +} + +} // namespace server +} // namespace http diff --git a/src/lib/http/request_parser.hpp b/src/lib/http/request_parser.hpp new file mode 100644 index 00000000000..828091c6bd3 --- /dev/null +++ b/src/lib/http/request_parser.hpp @@ -0,0 +1,89 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#ifndef HTTP_REQUEST_PARSER_HPP +#define HTTP_REQUEST_PARSER_HPP + +#include + +namespace http { +namespace server { + +struct request; + +/// Parser for incoming requests. +class request_parser +{ +public: + /// Construct ready to parse the request method. + request_parser(); + + /// Reset to initial parser state. + void reset(); + + /// Result of parse. + enum result_type { good, bad, indeterminate }; + + /// Parse some data. The enum return value is good when a complete request has + /// been parsed, bad if the data is invalid, indeterminate when more data is + /// required. The InputIterator return value indicates how much of the input + /// has been consumed. + template + std::tuple parse(request& req, + InputIterator begin, InputIterator end) + { + while (begin != end) + { + result_type result = consume(req, *begin++); + if (result == good || result == bad) + return std::make_tuple(result, begin); + } + return std::make_tuple(indeterminate, begin); + } + +private: + /// Handle the next character of input. + result_type consume(request& req, char input); + + /// Check if a byte is an HTTP character. + static bool is_char(int c); + + /// Check if a byte is an HTTP control character. + static bool is_ctl(int c); + + /// Check if a byte is defined as an HTTP tspecial character. + static bool is_tspecial(int c); + + /// Check if a byte is a digit. + static bool is_digit(int c); + + /// The current state of the parser. + enum state + { + method_start, + method, + uri, + http_version_h, + http_version_t_1, + http_version_t_2, + http_version_p, + http_version_slash, + http_version_major_start, + http_version_major, + http_version_minor_start, + http_version_minor, + expecting_newline_1, + header_line_start, + header_lws, + header_name, + space_before_header_value, + header_value, + expecting_newline_2, + expecting_newline_3 + } m_state; +}; + +} // namespace server +} // namespace http + +#endif // HTTP_REQUEST_PARSER_HPP diff --git a/src/lib/http/server.cpp b/src/lib/http/server.cpp new file mode 100644 index 00000000000..8c3c2e16967 --- /dev/null +++ b/src/lib/http/server.cpp @@ -0,0 +1,86 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#include "server.hpp" +#include +#include + +namespace http { +namespace server { + +server::server(const std::string& address, const std::string& port, const std::string& doc_root) + : m_io_context(1), + m_signals(m_io_context), + m_acceptor(m_io_context), + m_connection_manager(), + m_request_handler(doc_root) +{ + // Register to handle the signals that indicate when the server should exit. + // It is safe to register for the same signal multiple times in a program, + // provided all registration for the specified signal is made through Asio. + m_signals.add(SIGINT); + m_signals.add(SIGTERM); +#if defined(SIGQUIT) + signals_.add(SIGQUIT); +#endif // defined(SIGQUIT) + + do_await_stop(); + + // Open the acceptor with the option to reuse the address (i.e. SO_REUSEADDR). + asio::ip::tcp::resolver resolver(m_io_context); + asio::ip::tcp::endpoint endpoint = + *resolver.resolve(address, port).begin(); + m_acceptor.open(endpoint.protocol()); + m_acceptor.set_option(asio::ip::tcp::acceptor::reuse_address(true)); + m_acceptor.bind(endpoint); + m_acceptor.listen(); + + do_accept(); +} + +void server::run() +{ + // The io_context::run() call will block until all asynchronous operations + // have finished. While the server is running, there is always at least one + // asynchronous operation outstanding: the asynchronous accept call waiting + // for new incoming connections. + m_io_context.run(); +} + +void server::do_accept() +{ + m_acceptor.async_accept( + [this](std::error_code ec, asio::ip::tcp::socket socket) + { + // Check whether the server was stopped by a signal before this + // completion handler had a chance to run. + if (!m_acceptor.is_open()) + { + return; + } + + if (!ec) + { + m_connection_manager.start(std::make_shared( + std::move(socket), m_connection_manager, m_request_handler)); + } + + do_accept(); + }); +} + +void server::do_await_stop() +{ + m_signals.async_wait( + [this](std::error_code /*ec*/, int /*signo*/) + { + // The server is stopped by cancelling all outstanding asynchronous + // operations. Once all operations have finished the io_context::run() + // call will exit. + m_acceptor.close(); + m_connection_manager.stop_all(); + }); +} + +} // namespace server +} // namespace http diff --git a/src/lib/http/server.hpp b/src/lib/http/server.hpp new file mode 100644 index 00000000000..7c6483acfe3 --- /dev/null +++ b/src/lib/http/server.hpp @@ -0,0 +1,57 @@ +// license:Boost +// copyright-holders:Christopher M. Kohlhoff + +#ifndef HTTP_SERVER_HPP +#define HTTP_SERVER_HPP + +#include "asio.h" +#include +#include "connection.hpp" +#include "connection_manager.hpp" +#include "request_handler.hpp" + +namespace http { +namespace server { + +/// The top-level class of the HTTP server. +class server +{ +public: + server(const server&) = delete; + server& operator=(const server&) = delete; + + /// Construct the server to listen on the specified TCP address and port, and + /// serve up files from the given directory. + explicit server(const std::string& address, const std::string& port, + const std::string& doc_root); + + /// Run the server's io_context loop. + void run(); + +private: + /// Perform an asynchronous accept operation. + void do_accept(); + + /// Wait for a request to stop the server. + void do_await_stop(); + + /// The io_context used to perform asynchronous operations. + asio::io_context m_io_context; + + /// The signal_set is used to register for process termination notifications. + asio::signal_set m_signals; + + /// Acceptor used to listen for incoming connections. + asio::ip::tcp::acceptor m_acceptor; + + /// The connection manager which owns all live connections. + connection_manager m_connection_manager; + + /// The handler for all incoming requests. + request_handler m_request_handler; +}; + +} // namespace server +} // namespace http + +#endif // HTTP_SERVER_HPP