diff --git a/CMakeLists.txt b/CMakeLists.txt index 802e15bb..1c99676d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,10 @@ file(GLOB dvmhost_SRC "network/*.h" "network/*.cpp" "network/json/*.h" + "network/rest/*.h" + "network/rest/*.cpp" + "network/rest/http/*.h" + "network/rest/http/*.cpp" "remote/RemoteCommand.cpp" "remote/RemoteCommand.h" "yaml/*.h" @@ -294,6 +298,12 @@ else () message(CHECK_PASS "no") endif (CROSS_COMPILE_RPI_ARM) +option(WITH_ASIO "Specifies the location for the ASIO library" off) +if (WITH_ASIO) + set(ASIO_INCLUDE_DIR ${WITH_ASIO}/include) + message(CHECK_START "With ASIO: ${ASIO_INCLUDE_DIR}") +endif (WITH_ASIO) + # # standard CMake options # @@ -321,6 +331,14 @@ add_definitions(-D__GIT_VER_HASH__="${GIT_VER_HASH}") # dvmhost project project(dvmhost) find_package(Threads REQUIRED) +if (NOT WITH_ASIO) + find_package(ASIO REQUIRED) +else() + add_library(asio::asio INTERFACE IMPORTED) + target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR}) + target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE") + target_link_libraries(asio::asio INTERFACE Threads::Threads) +endif () add_executable(dvmhost ${dvmhost_SRC}) target_include_directories(dvmhost PRIVATE .) target_link_libraries(dvmhost PRIVATE Threads::Threads util) @@ -354,6 +372,7 @@ target_include_directories(dvmcmd PRIVATE .) if (ENABLE_TESTS) # dvmtest project project(dvmtest) +set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") Include(FetchContent) FetchContent_Declare( @@ -364,6 +383,14 @@ FetchContent_Declare( FetchContent_MakeAvailable(Catch2) find_package(Threads REQUIRED) +if (NOT WITH_ASIO) + find_package(ASIO REQUIRED) +else() + add_library(asio::asio INTERFACE IMPORTED) + target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR}) + target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE") + target_link_libraries(asio::asio INTERFACE Threads::Threads) +endif () add_executable(dvmtests ${dvmhost_SRC} ${dvmtests_SRC}) target_compile_definitions(dvmtests PUBLIC -DCATCH2_TEST_COMPILATION) target_link_libraries(dvmtests PRIVATE Catch2::Catch2WithMain Threads::Threads util) diff --git a/README.md b/README.md index 5e102e03..319499ef 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,13 @@ This project utilizes CMake for its build system. (All following information ass The DVM Host software does not have any specific library dependancies and is written to be as library-free as possible. A basic GCC/G++ install is usually all thats needed to compile. +## Dependancies + +This project requires the ASIO library (https://think-async.com/Asio/) for its REST API services. This can be installed on most Debian/Ubuntu Linux's with: ```apt-get install libasio-dev``` + +Alternatively, if you download the ASIO library from the ASIO website and extract it to a location, you can specify the path to the ASIO library using: ```-DWITH_ASIO=/path/to/asio```. This method is required when cross-compiling +for old Raspberry Pi ARM 32 bit. + ### Build Instructions 1. Clone the repository. ```git clone https://github.com/DVMProject/dvmhost.git``` 2. Switch into the "dvmhost" folder. Create a new folder named "build" and switch into it. diff --git a/cmake/FindASIO.cmake b/cmake/FindASIO.cmake new file mode 100644 index 00000000..6eba158e --- /dev/null +++ b/cmake/FindASIO.cmake @@ -0,0 +1,40 @@ +# +# Finds the ASIO library. +# +# from https://think-async.com/Asio/ +# +# This will define the following variables +# +# ASIO_FOUND +# ASIO_INCLUDE_DIR +# +# and the following imported targets +# +# asio::asio +# + +find_package(Threads QUIET) +if (Threads_FOUND) + find_path(ASIO_INCLUDE_DIR asio.hpp) + + mark_as_advanced(ASIO_FOUND ASIO_INCLUDE_DIR) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(ASIO + FOUND_VAR ASIO_FOUND + REQUIRED_VARS ASIO_INCLUDE_DIR + ) + + if(ASIO_FOUND AND NOT TARGET asio::asio) + add_library(asio::asio INTERFACE IMPORTED) + target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR}) + target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE") + target_link_libraries(asio::asio INTERFACE Threads::Threads) + endif() +else() + if(asio_FIND_REQUIRED) + message(FATAL_ERROR "asio requires Threads, which couldn't be found.") + elseif(asio_FIND_QUIETLY) + message(STATUS "asio requires Threads, which couldn't be found.") + endif() +endif() diff --git a/network/rest/Dispatcher.h b/network/rest/Dispatcher.h new file mode 100644 index 00000000..e8b2c4fd --- /dev/null +++ b/network/rest/Dispatcher.h @@ -0,0 +1,177 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#if !defined(__REST__DISPATHER_H__) +#define __REST__DISPATHER_H__ + +#include "Defines.h" + +#include +#include +#include +#include +#include + +namespace rest { + // --------------------------------------------------------------------------- + // Structure Declaration + // + // --------------------------------------------------------------------------- + + template + struct RequestMatch : Matched + { + /// Initializes a new instance of the RequestMatch structure. + RequestMatch(const Matched& m, const std::string& d) : Matched(m) , data(d) { /* stub */ } + + std::string data; + }; + + // --------------------------------------------------------------------------- + // Structure Declaration + // + // --------------------------------------------------------------------------- + + template + struct RequestMatcher { + typedef std::function&)> RequestHandlerType; + typedef RequestMatcher selfType; + + /// Initializes a new instance of the RequestMatch structure. + explicit RequestMatcher(const Regex& expression) : m_expression(expression) { /* stub */ } + + /// + selfType& get(RequestHandlerType handler) { + m_handlers["GET"] = handler; + return *this; + } + /// + selfType& post(RequestHandlerType handler) { + m_handlers["POST"] = handler; + return *this; + } + /// + selfType& put(RequestHandlerType handler) { + m_handlers["PUT"] = handler; + return *this; + } + /// + selfType& del(RequestHandlerType handler) { + m_handlers["DELETE"] = handler; + return *this; + } + /// + selfType& options(RequestHandlerType handler) { + m_handlers["OPTIONS"] = handler; + return *this; + } + + /// + template + void handleRequest(const Request& request, Response& response, const Matched &what) { + // dispatching to matching based on handler + RequestMatch match(what, request.data); + auto& handler = m_handlers[request.method]; + if (handler) { + handler(response, match); + } + } + + private: + Regex m_expression; + std::map m_handlers; + }; + + // --------------------------------------------------------------------------- + // Class Declaration + // This class implements RESTful web request dispatching. + // --------------------------------------------------------------------------- + + template + class RequestDispatcher { + typedef RequestMatcher MatcherType; + typedef std::shared_ptr MatcherTypePtr; + public: + /// Initializes a new instance of the RequestDispatcher class. + RequestDispatcher() : m_basePath() { /* stub */ } + /// Initializes a new instance of the RequestDispatcher class. + RequestDispatcher(const std::string &basePath) : m_basePath(basePath) { /* stub */ } + + /// + MatcherType& match(const Expression& expression) + { + MatcherTypePtr& p = m_matchers[expression]; + if(!p) { + p = std::make_shared(expression); + } + + return *p; + } + + /// + template + typename std::enable_if::value, void>::type + handleRequest(const Request& request, Response& response) + { + for (const auto& matcher : m_matchers) { + Match what; + if (std::regex_match(request.uri.c_str(), what, matcher.first)) { + matcher.second->handle_request(request, response, what); + } + } + } + + /// + template + typename std::enable_if::value, void>::type + handleRequest(const Request& request, Response& response) + { + for ( const auto& matcher : m_matchers) { + Match what; + if (request.uri.find(matcher.first) != std::string::npos) { + what = matcher.first; + matcher.second->handle_request(request, response, what); + } + } + } + + private: + std::string m_basePath; + std::map m_matchers; + }; +} // namespace rest + +#endif // __REST__DISPATHER_H__ diff --git a/network/rest/http/Connection.h b/network/rest/http/Connection.h new file mode 100644 index 00000000..fdd5d60e --- /dev/null +++ b/network/rest/http/Connection.h @@ -0,0 +1,174 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#if !defined(__REST_HTTP__CONNECTION_H__) +#define __REST_HTTP__CONNECTION_H__ + +#include "Defines.h" +#include "network/rest/http/HTTPReply.h" +#include "network/rest/http/HTTPRequest.h" +#include "network/rest/http/HTTPRequestLexer.h" + +#include +#include +#include +#include +#include + +namespace rest { + namespace server { + + // --------------------------------------------------------------------------- + // Class Prototypes + // --------------------------------------------------------------------------- + + template class ConnectionManager; + + // --------------------------------------------------------------------------- + // Class Declaration + // This class represents a single connection from a client. + // --------------------------------------------------------------------------- + + template + class Connection : public std::enable_shared_from_this> + { + typedef Connection selfType; + typedef std::shared_ptr selfTypePtr; + typedef ConnectionManager ConnectionManagerType; + public: + /// Initializes a new instance of the Connection class. + explicit Connection(asio::ip::tcp::socket socket, ConnectionManagerType& manager, RequestHandlerType& handler, + bool persistent = false) : + m_socket(std::move(socket)), + m_connectionManager(manager), + m_requestHandler(handler), + m_persistent(persistent) + { + /* stub */ + } + /// Initializes a copy instance of the Connection class. + Connection(const Connection&) = delete; + + /// + Connection& operator=(const Connection&) = delete; + + /// Start the first asynchronous operation for the connection. + void start() { read(); } + /// Stop all asynchronous operations associated with the connection. + void stop() { m_socket.close(); } + private: + /// Perform an asynchronous read operation. + void read() + { + if (!m_persistent) { + auto self(this->shared_from_this()); + } + + m_socket.async_read_some(asio::buffer(m_buffer), [=](asio::error_code ec, std::size_t bytes_transferred) { + if (!ec) { + HTTPRequestLexer::ResultType result; + char* data; + + std::tie(result, data) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + bytes_transferred); + auto itr = std::find_if(m_request.headers.begin(), m_request.headers.end(), [](const header &h) { return h.name == "content-length"; }); + if (itr != request_.headers.end()) { + m_request.data = std::string(data, std::static_cast(itr->value)); + } + + if (result == HTTPRequestLexer::GOOD) { + m_requestHandler.handleRequest(m_request, m_reply); + write(); + } + else if (result == HTTPRequestLexer::BAD) { + m_reply = reply::stockReply(HTTPReply::BAD_REQUEST); + write(); + } + else { + read(); + } + } + else if (ec != asio::error::operation_aborted) { + m_connectionManager.stop(this->shared_from_this()); + } + }); + } + + /// Perform an asynchronous write operation. + void write() + { + if (!m_persistent) { + auto self(this->shared_from_this()); + } else { + m_reply.headers.emplace_back("Connection:", "keep-alive"); + } + + asio::async_write(m_socket, m_reply.to_buffers(), [=](asio::error_code ec, std::size_t) { + if (m_persistent) { + m_lexer.reset(); + m_reply.headers.resize(0); + m_reply.status = HTTPReply::OK; + m_reply.content = ""; + m_request = HTTPRequest(); + read(); + } + else + { + 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_connectionManager.stop(this->shared_from_this()); + } + } + }); + } + + asio::ip::tcp::socket m_socket; + ConnectionManagerType& m_connectionManager; + RequestHandlerType& m_requestHandler; + std::array m_buffer; + HTTPRequest m_request; + HTTPRequestLexer m_lexer; + HTTPReply m_reply; + bool m_persistent; + }; + } // namespace server +} // namespace rest + +#endif // __REST_HTTP__CONNECTION_H__ diff --git a/network/rest/http/ConnectionManager.h b/network/rest/http/ConnectionManager.h new file mode 100644 index 00000000..e14a9679 --- /dev/null +++ b/network/rest/http/ConnectionManager.h @@ -0,0 +1,104 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#if !defined(__REST_HTTP__CONNECTION_MANAGER_H__) +#define __REST_HTTP__CONNECTION_MANAGER_H__ + +#include "Defines.h" + +#include +#include + +namespace rest { + namespace server { + + // --------------------------------------------------------------------------- + // Class Declaration + // Manages open connections so that they may be cleanly stopped when the server + // needs to shut down. + // --------------------------------------------------------------------------- + + template + class ConnectionManager + { + public: + /// Initializes a new instance of the ConnectionManager class. + ConnectionManager() { /* stub */ } + /// Initializes a copy instance of the ConnectionManager class. + ConnectionManager(const ConnectionManager&) = delete; + + /// + ConnectionManager& operator=(const ConnectionManager&) = delete; + + /// Add the specified connection to the manager and start it. + void start(ConnectionPtr c) + { + std::lock_guard guard(m_lock); + { + m_connections.insert(c); + } + c->start(); + } + + /// Stop the specified connection. + void stop(connection_ptr c) + { + std::lock_guard guard(m_lock); + { + m_connections.erase(c); + } + c->stop(); + } + + /// Stop all connections. + void stopAll() + { + for (auto c : m_connections) + c->stop(); + + std::lock_guard guard(m_lock); + m_connections.clear(); + } + + private: + std::set m_connections; + std::mutex m_lock; + }; + } // namespace server +} // namespace rest + +#endif // __REST_HTTP__CONNECTION_MANAGER_H__ + \ No newline at end of file diff --git a/network/rest/http/HTTPHeader.h b/network/rest/http/HTTPHeader.h new file mode 100644 index 00000000..5af9d919 --- /dev/null +++ b/network/rest/http/HTTPHeader.h @@ -0,0 +1,66 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#if !defined(__REST_HTTP__HTTP_HEADER_H__) +#define __REST_HTTP__HTTP_HEADER_H__ + +#include "Defines.h" + +#include + +namespace rest { + namespace server { + + // --------------------------------------------------------------------------- + // Structure Declaration + // This class implements a model for an HTTP header. + // --------------------------------------------------------------------------- + + struct HTTPHeader + { + std::string name; + std::string value; + + /// Initializes a new instance of the HTTPHeader struct. + HTTPHeader() { /* stub */ } + /// Initializes a new instance of the HTTPHeader struct. + HTTPHeader(const std::string& name, const std::string& value) : name{name}, value{value} { /* stub */ } + }; + + } // namespace server +} // namespace rest + +#endif // __REST_HTTP__HTTP_HEADER_H__ diff --git a/network/rest/http/HTTPReply.cpp b/network/rest/http/HTTPReply.cpp new file mode 100644 index 00000000..c1a7bd6f --- /dev/null +++ b/network/rest/http/HTTPReply.cpp @@ -0,0 +1,267 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#include "Defines.h" +#include "network/rest/http/HTTPReply.h" + +using namespace rest::server; + +#include + +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 toBuffer(HTTPReply::StatusType status) + { + switch (status) + { + case HTTPReply::OK: + return asio::buffer(ok); + case HTTPReply::CREATED: + return asio::buffer(created); + case HTTPReply::ACCEPTED: + return asio::buffer(accepted); + case HTTPReply::NO_CONTENT: + return asio::buffer(no_content); + case HTTPReply::MULTIPLE_CHOICES: + return asio::buffer(multiple_choices); + case HTTPReply::MOVED_PERMANENTLY: + return asio::buffer(moved_permanently); + case HTTPReply::MOVED_TEMPORARILY: + return asio::buffer(moved_temporarily); + case HTTPReply::NOT_MODIFIED: + return asio::buffer(not_modified); + case HTTPReply::BAD_REQUEST: + return asio::buffer(bad_request); + case HTTPReply::UNAUTHORIZED: + return asio::buffer(unauthorized); + case HTTPReply::FORBIDDEN: + return asio::buffer(forbidden); + case HTTPReply::NOT_FOUND: + return asio::buffer(not_found); + case HTTPReply::INTERNAL_SERVER_ERROR: + return asio::buffer(internal_server_error); + case HTTPReply::NOT_IMPLEMENTED: + return asio::buffer(not_implemented); + case HTTPReply::BAD_GATEWAY: + return asio::buffer(bad_gateway); + case HTTPReply::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 HTTPReply::toBuffers() +{ + std::vector buffers; + buffers.push_back(status_strings::toBuffer(status)); + + for (std::size_t i = 0; i < headers.size(); ++i) { + HTTPHeader& 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(HTTPReply::StatusType status) + { + switch (status) + { + case HTTPReply::OK: + return ok; + case HTTPReply::CREATED: + return created; + case HTTPReply::ACCEPTED: + return accepted; + case HTTPReply::NO_CONTENT: + return no_content; + case HTTPReply::MULTIPLE_CHOICES: + return multiple_choices; + case HTTPReply::MOVED_PERMANENTLY: + return moved_permanently; + case HTTPReply::MOVED_TEMPORARILY: + return moved_temporarily; + case HTTPReply::NOT_MODIFIED: + return not_modified; + case HTTPReply::BAD_REQUEST: + return bad_request; + case HTTPReply::UNAUTHORIZED: + return unauthorized; + case HTTPReply::FORBIDDEN: + return forbidden; + case HTTPReply::NOT_FOUND: + return not_found; + case HTTPReply::INTERNAL_SERVER_ERROR: + return internal_server_error; + case HTTPReply::NOT_IMPLEMENTED: + return not_implemented; + case HTTPReply::BAD_GATEWAY: + return bad_gateway; + case HTTPReply::SERVICE_UNAVAILABLE: + return service_unavailable; + default: + return internal_server_error; + } + } +} // namespace stock_replies + +HTTPReply HTTPReply::stockReply(HTTPReply::StatusType status, const char* mime) +{ + HTTPReply rep; + rep.status = status; + + if (status != HTTPReply::NO_CONTENT) { + 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 = mime; + } + + return rep; +} + +HTTPReply& operator<<(HTTPReply& r, const std::string &value) { + r.content.append(value); + return r; +} diff --git a/network/rest/http/HTTPReply.h b/network/rest/http/HTTPReply.h new file mode 100644 index 00000000..967b950b --- /dev/null +++ b/network/rest/http/HTTPReply.h @@ -0,0 +1,93 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#if !defined(__REST_HTTP__HTTP_REPLY_H__) +#define __REST_HTTP__HTTP_REPLY_H__ + +#include "Defines.h" +#include "network/rest/http/HTTPHeader.h" + +#include +#include + +#include + +namespace rest { + namespace server { + + // --------------------------------------------------------------------------- + // Structure Declaration + // This struct implements a model of a reply to be sent to a HTTP client. + // --------------------------------------------------------------------------- + + struct HTTPReply + { + enum StatusType { + 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; + + std::vector headers; + 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 toBuffers(); + + /// Get a stock reply. + static HTTPReply stockReply(StatusType status, const char* mime = "text/html"); + }; + + HTTPReply& operator<<(HTTPReply& r, const std::string& value); + } // namespace server +} // namespace rest + +#endif // __REST_HTTP__HTTP_REPLY_H__ diff --git a/network/rest/http/HTTPRequest.h b/network/rest/http/HTTPRequest.h new file mode 100644 index 00000000..52677e0a --- /dev/null +++ b/network/rest/http/HTTPRequest.h @@ -0,0 +1,68 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#if !defined(__REST_HTTP__HTTP_REQUEST_H__) +#define __REST_HTTP__HTTP_REQUEST_H__ + +#include "Defines.h" +#include "network/rest/http/HTTPHeader.h" + +#include +#include + +namespace rest { + namespace server { + + // --------------------------------------------------------------------------- + // Structure Declaration + // This struct implements a model of a request received from a HTTP client. + // --------------------------------------------------------------------------- + + struct HTTPRequest + { + std::string method; + std::string uri; + + int httpVersionMajor; + int httpVersionMinor; + + std::vector headers; + std::string data; + }; + } // namespace server +} // namespace http + +#endif // __REST_HTTP__HTTP_REQUEST_H__ diff --git a/network/rest/http/HTTPRequestHandler.cpp b/network/rest/http/HTTPRequestHandler.cpp new file mode 100644 index 00000000..05351b55 --- /dev/null +++ b/network/rest/http/HTTPRequestHandler.cpp @@ -0,0 +1,152 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#include "Defines.h" +#include "network/rest/http/HTTPRequestHandler.h" +#include "network/rest/http/HTTPRequest.h" +#include "network/rest/http/HTTPReply.h" + +using namespace rest::server; + +#include +#include +#include + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/// +/// Initializes a new instance of the HTTPRequestHandler class. +/// +/// +HTTPRequestHandler::HTTPRequestHandler(const std::string& docRoot) : + m_docRoot(docRoot) +{ + /* stub */ +} + +/// +/// Handle a request and produce a reply. +/// +void HTTPRequestHandler::handleRequest(const HTTPRequest& request, HTTPReply& reply) +{ + // decode url to path + std::string requestPath; + if (!urlDecode(request.uri, requestPath)) { + reply = HTTPReply::stockReply(HTTPReply::BAD_REQUEST); + return; + } + + // request path must be absolute and not contain "..". + if (requestPath.empty() || requestPath[0] != '/' || + requestPath.find("..") != std::string::npos) { + reply = HTTPReply::stockReply(HTTPReply::BAD_REQUEST); + return; + } + + // if path ends in slash (i.e. is a directory) then add "index.html" + if (requestPath[requestPath.size() - 1] == '/') { + requestPath += "index.html"; + } + + // determine the file extension + std::size_t lastSlashPos = requestPath.find_last_of("/"); + std::size_t lastDotPos = requestPath.find_last_of("."); + std::string extension; + if (lastDotPos != std::string::npos && lastDotPos > lastSlashPos) { + extension = requestPath.substr(lastDotPos + 1); + } + + // open the file to send back + std::string fullPath = m_docRoot + requestPath; + std::ifstream is(fullPath.c_str(), std::ios::in | std::ios::binary); + if (!is) { + reply = HTTPReply::stockReply(HTTPReply::NOT_FOUND); + return; + } + + // fill out the reply to be sent to the client + reply.status = HTTPReply::OK; + + char buf[512]; + while (is.read(buf, sizeof(buf)).gcount() > 0) + reply.content.append(buf, is.gcount()); + + reply.headers.resize(2); + reply.headers[0].name = "Content-Length"; + reply.headers[0].value = std::to_string(reply.content.size()); + reply.headers[1].name = "Content-Type"; + reply.headers[1].value = "application/octet-stream"; +} + +/// +/// Perform URL-decoding on a string. Returns false if the encoding was invalid. +/// +/// +/// +bool HTTPRequestHandler::urlDecode(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; +} diff --git a/network/rest/http/HTTPRequestHandler.h b/network/rest/http/HTTPRequestHandler.h new file mode 100644 index 00000000..ff66ba0e --- /dev/null +++ b/network/rest/http/HTTPRequestHandler.h @@ -0,0 +1,87 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#if !defined(__REST_HTTP__HTTP_REQUEST_HANDLER_H__) +#define __REST_HTTP__HTTP_REQUEST_HANDLER_H__ + +#include "Defines.h" + +#include + +namespace rest { + namespace server { + + // --------------------------------------------------------------------------- + // Class Prototypes + // --------------------------------------------------------------------------- + + struct HTTPReply; + struct HTTPRequest; + + // --------------------------------------------------------------------------- + // Class Declaration + // This class implements the common handler for all incoming requests. + // --------------------------------------------------------------------------- + + class HTTPRequestHandler + { + public: + /// Initializes a new instance of the HTTPRequestHandler class. + explicit HTTPRequestHandler(const std::string& docRoot); + /// Initializes a copy instance of the HTTPRequestHandler class. + HTTPRequestHandler(const HTTPRequestHandler&) = delete; + /// + HTTPRequestHandler(HTTPRequestHandler&&) = default; + + /// + HTTPRequestHandler& operator=(const HTTPRequestHandler&) = delete; + /// + HTTPRequestHandler& operator=(HTTPRequestHandler&&) = default; + + /// Handle a request and produce a reply. + void handleRequest(const HTTPRequest& req, HTTPReply& reply); + + private: + /// Perform URL-decoding on a string. Returns false if the encoding was + /// invalid. + static bool urlDecode(const std::string& in, std::string& out); + + std::string m_docRoot; + }; + } // namespace server +} // namespace rest + +#endif // __REST_HTTP__HTTP_REQUEST_HANDLER_H__ diff --git a/network/rest/http/HTTPRequestLexer.cpp b/network/rest/http/HTTPRequestLexer.cpp new file mode 100644 index 00000000..1873393a --- /dev/null +++ b/network/rest/http/HTTPRequestLexer.cpp @@ -0,0 +1,361 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#include "Defines.h" +#include "network/rest/http/HTTPRequestLexer.h" +#include "network/rest/http/HTTPRequest.h" + +using namespace rest::server; + +#include + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/// +/// Initializes a new instance of the HTTPRequestLexer class. +/// + +HTTPRequestLexer::HTTPRequestLexer() : + m_state(METHOD_START) +{ + /* stub */ +} + +/// Reset to initial parser state. +void HTTPRequestLexer::reset() +{ + m_state = METHOD_START; +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/// +/// Handle the next character of input. +/// +/// +/// +/// +HTTPRequestLexer::ResultType HTTPRequestLexer::consume(HTTPRequest& req, char input) +{ + switch (m_state) + { + /* + ** HTTP Method + */ + + case METHOD_START: + if (!isChar(input) || isControl(input) || isSpecial(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 (!isChar(input) || isControl(input) || isSpecial(input)) { + return BAD; + } + else { + req.method.push_back(input); + return INDETERMINATE; + } + + /* + ** URI + */ + + case URI: + if (input == ' ') { + m_state = HTTP_VERSION_H; + return INDETERMINATE; + } + else if (isControl(input)) { + return BAD; + } + else { + req.uri.push_back(input); + return INDETERMINATE; + } + + /* + ** HTTP/1.0 + */ + 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.httpVersionMajor = 0; + req.httpVersionMinor = 0; + m_state = HTTP_VERSION_MAJOR_START; + return INDETERMINATE; + } + else { + return BAD; + } + case HTTP_VERSION_MAJOR_START: + if (isDigit(input)) { + req.httpVersionMajor = req.httpVersionMajor * 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 (isDigit(input)) { + req.httpVersionMajor = req.httpVersionMajor * 10 + input - '0'; + return INDETERMINATE; + } + else { + return BAD; + } + case HTTP_VERSION_MINOR_START: + if (isDigit(input)) { + req.httpVersionMinor = req.httpVersionMinor * 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 (isDigit(input)) + { + req.httpVersionMinor = req.httpVersionMinor * 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; + } + + /* + ** Headers + */ + + 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 (!isChar(input) || isControl(input) || isSpecial(input)) { + return BAD; + } + else { + req.headers.push_back(HTTPHeader()); + req.headers.back().name.push_back(std::tolower(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 (isControl(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 (!isChar(input) || isControl(input) || isSpecial(input)) { + return BAD; + } + else + { + req.headers.back().name.push_back(std::tolower(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 (isControl(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; + } +} + +/// +/// Check if a byte is an HTTP character. +/// +/// +/// +bool HTTPRequestLexer::isChar(int c) +{ + return c >= 0 && c <= 127; +} + +/// +/// Check if a byte is an HTTP control character. +/// +/// +/// +bool HTTPRequestLexer::isControl(int c) +{ + return (c >= 0 && c <= 31) || (c == 127); +} + +/// +/// Check if a byte is an HTTP special character. +/// +/// +/// +bool HTTPRequestLexer::isSpecial(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; + } +} + +/// +/// Check if a byte is an digit. +/// +/// +/// +bool HTTPRequestLexer::isDigit(int c) +{ + return c >= '0' && c <= '9'; +} diff --git a/network/rest/http/HTTPRequestLexer.h b/network/rest/http/HTTPRequestLexer.h new file mode 100644 index 00000000..2beaf44e --- /dev/null +++ b/network/rest/http/HTTPRequestLexer.h @@ -0,0 +1,122 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#if !defined(__REST_HTTP__HTTP_REQUEST_PARSER_H__) +#define __REST_HTTP__HTTP_REQUEST_PARSER_H__ + +#include + +namespace rest { + namespace server { + + // --------------------------------------------------------------------------- + // Class Prototypes + // --------------------------------------------------------------------------- + + struct HTTPRequest; + + // --------------------------------------------------------------------------- + // Class Declaration + // This class implements the lexer for incoming requests. + // --------------------------------------------------------------------------- + + class HTTPRequestLexer + { + public: + enum ResultType { GOOD, BAD, INDETERMINATE }; + + /// Initializes a new instance of the HTTPRequestLexer class. + HTTPRequestLexer(); + + /// Reset to initial parser state. + void reset(); + + /// 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(HTTPRequest& req, InputIterator begin, InputIterator end) + { + while (begin != end) { + ResultType 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. + ResultType consume(HTTPRequest& req, char input); + + /// Check if a byte is an HTTP character. + static bool isChar(int c); + /// Check if a byte is an HTTP control character. + static bool isControl(int c); + /// Check if a byte is an HTTP special character. + static bool isSpecial(int c); + /// Check if a byte is an digit. + static bool isDigit(int c); + + 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 rest + +#endif // __REST_HTTP__HTTP_REQUEST_PARSER_H__ diff --git a/network/rest/http/HTTPServer.h b/network/rest/http/HTTPServer.h new file mode 100644 index 00000000..6d0538cf --- /dev/null +++ b/network/rest/http/HTTPServer.h @@ -0,0 +1,160 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + #if !defined(__REST_HTTP__HTTP_SERVER_H__) +#define __REST_HTTP__HTTP_SERVER_H__ + +#include "Defines.h" +#include "network/rest/Connection.h" +#include "network/rest/ConnectionManager.h" +#include "network/rest/HTTPRequestHandler.h" + +#include + +#include +#include +#include +#include +#include + +namespace rest { + namespace server { + + // --------------------------------------------------------------------------- + // Class Declaration + // This class implements top-level routines of the HTTP server. + // --------------------------------------------------------------------------- + + template class ConnectionImpl = Connection> + class HTTPServer { + public: + /// Initializes a new instance of the HTTPServer class. + template + explicit HTTPServer(const std::string& address, const std::string& port, Handler&& handler) : + m_ioService(), + m_signals(m_ioService), + m_acceptor(m_ioService), + m_connectionManager(), + m_socket(m_ioService), + m_requestHandler(std::forward(handler)) + { + // 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) + m_signals.add(SIGQUIT); +#endif + + awaitStop(); + + // open the acceptor with the option to reuse the address (i.e. SO_REUSEADDR) + asio::ip::tcp::resolver resolver(m_ioService); + asio::ip::tcp::endpoint endpoint = *resolver.resolve({address, port}); + + m_acceptor.open(endpoint.protocol()); + m_acceptor.set_option(asio::ip::tcp::acceptor::reuse_address(true)); + m_acceptor.set_option(asio::socket_base::keep_alive(true)); + m_acceptor.bind(endpoint); + m_acceptor.listen(); + + accept(); + } + /// Initializes a copy instance of the HTTPServer class. + HTTPServer(const HTTPServer&) = delete; + + /// + HTTPServer& operator=(const HTTPServer&) = delete; + + /// Run the servers ASIO IO service loop. + void run() + { + // the 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_ioService.run(); + } + + private: + /// Perform an asynchronous accept operation. + void accept() + { + m_acceptor.async_accept(m_socket, [this](asio::error_code ec) { + // 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_connectionManager.start(std::make_shared(std::move(m_socket), m_connectionManager, m_requestHandler)); + } + + accept(); + }); + } + + /// Wait for a request to stop the server. + void awaitStop() + { + m_signals.async_wait([this](asio::error_code /*ec*/, int /*signo*/) { + // the server is stopped by cancelling all outstanding asynchronous + // operations; once all operations have finished the m_ioService::run() + // call will exit + m_acceptor.close(); + m_connectionManager.stopAll(); + }); + } + + typedef ConnectionImpl ConnectionType; + typedef std::shared_ptr ConnectionTypePtr; + + asio::io_service m_ioService; + asio::signal_set m_signals; + asio::ip::tcp::acceptor m_acceptor; + + ConnectionManager m_connectionManager; + + asio::ip::tcp::socket m_socket; + + RequestHandlerType m_requestHandler; + }; + } // namespace server +} // namespace rest + +#endif // __REST_HTTP__HTTP_SERVER_H__