diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index c2344bfe5..d66833d40 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -23,7 +23,7 @@ jobs: windows-latest, macos-latest, ubuntu-20.04, - ubuntu-24.04, + ubuntu-22.04, macos-13 ] # ubuntu-18.04 does not work due to compile error on asio diff --git a/.github/workflows/submit_coverage.yml b/.github/workflows/submit_coverage.yml index c768b680a..04758a77b 100644 --- a/.github/workflows/submit_coverage.yml +++ b/.github/workflows/submit_coverage.yml @@ -16,7 +16,7 @@ jobs: if: github.event.workflow_run.conclusion == 'success' steps: - name: Download artifact - uses: dawidd6/action-download-artifact@v7 + uses: dawidd6/action-download-artifact@v8 with: workflow: ${{ github.event.workflow_run.workflow_id }} workflow_conclusion: success diff --git a/docs/guides/testing.md b/docs/guides/testing.md index 377432221..f7c49ba51 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -15,7 +15,10 @@ Crow allows users to handle requests that may not come from the network. This is req.url = "/place"; - app.handle(req, res); //res will contain a code of 200, and a response body of "hi" + app.handle_full(req, res); + // res will contain: + // res.code == 200 + // res.body == "hi" } ``` diff --git a/include/crow/app.h b/include/crow/app.h index 5cad7b708..8c244e1cb 100644 --- a/include/crow/app.h +++ b/include/crow/app.h @@ -380,7 +380,7 @@ namespace crow } /// \brief Get the number of threads that server is using - std::uint16_t concurrency() + std::uint16_t concurrency() const { return concurrency_; } @@ -521,12 +521,18 @@ namespace crow #endif validate(); + error_code ec; + asio::ip::address addr = asio::ip::make_address(bindaddr_,ec); + if (ec){ + CROW_LOG_ERROR << ec.message() << " - Can not create valid ip address from string: \"" << bindaddr_ << "\""; + return; + } + tcp::endpoint endpoint(addr, port_); #ifdef CROW_ENABLE_SSL if (ssl_used_) { router_.using_ssl = true; - TCPAcceptor::endpoint endpoint(asio::ip::address::from_string(bindaddr_), port_); ssl_server_ = std::move(std::unique_ptr(new ssl_server_t(this, endpoint, server_name_, &middlewares_, concurrency_, timeout_, &ssl_context_))); ssl_server_->set_tick_function(tick_interval_, tick_function_); ssl_server_->signal_clear(); @@ -729,21 +735,32 @@ namespace crow } /// \brief Wait until the server has properly started - void wait_for_server_start() + std::cv_status wait_for_server_start(std::chrono::milliseconds wait_timeout = std::chrono::milliseconds(3000)) { + std::cv_status status = std::cv_status::no_timeout; + auto wait_until = std::chrono::steady_clock::now() + wait_timeout; { std::unique_lock lock(start_mutex_); - while (!server_started_) - cv_started_.wait(lock); + while (!server_started_ && (status == std::cv_status::no_timeout)) + { + status = cv_started_.wait_until(lock, wait_until); + } } - if (server_) - server_->wait_for_start(); - else if (unix_server_) - unix_server_->wait_for_start(); + if (status == std::cv_status::no_timeout) + { + if (server_) { + status = server_->wait_for_start(wait_until); + } else if (unix_server_) { + status = unix_server_->wait_for_start(wait_until); + } #ifdef CROW_ENABLE_SSL - else if (ssl_server_) - ssl_server_->wait_for_start(); + else if (ssl_server_) + { + status = ssl_server_->wait_for_start(wait_until); + } #endif + } + return status; } private: diff --git a/include/crow/http_connection.h b/include/crow/http_connection.h index 307a9222c..1391b5108 100644 --- a/include/crow/http_connection.h +++ b/include/crow/http_connection.h @@ -128,7 +128,7 @@ namespace crow buffers_.clear(); static std::string expect_100_continue = "HTTP/1.1 100 Continue\r\n\r\n"; buffers_.emplace_back(expect_100_continue.data(), expect_100_continue.size()); - do_write(); + do_write_sync(buffers_); } } @@ -425,7 +425,7 @@ namespace crow res_body_copy_.swap(res.body); buffers_.emplace_back(res_body_copy_.data(), res_body_copy_.size()); - do_write(); + do_write_sync(buffers_); if (need_to_start_read_after_complete_) { diff --git a/include/crow/http_server.h b/include/crow/http_server.h index b44a298cd..d25ad96af 100644 --- a/include/crow/http_server.h +++ b/include/crow/http_server.h @@ -43,8 +43,14 @@ namespace crow // NOTE: Already documented in "crow/app.h" class Server { public: - Server(Handler* handler, typename Acceptor::endpoint endpoint, std::string server_name = std::string("Crow/") + VERSION, std::tuple* middlewares = nullptr, uint16_t concurrency = 1, uint8_t timeout = 5, typename Adaptor::context* adaptor_ctx = nullptr): - acceptor_(io_context_, endpoint), + Server(Handler* handler, + typename Acceptor::endpoint endpoint, + std::string server_name = std::string("Crow/") + VERSION, + std::tuple* middlewares = nullptr, + uint16_t concurrency = 1, + uint8_t timeout = 5, + typename Adaptor::context* adaptor_ctx = nullptr): + acceptor_(io_context_,endpoint), signals_(io_context_), tick_timer_(io_context_), handler_(handler), @@ -148,11 +154,10 @@ namespace crow // NOTE: Already documented in "crow/app.h" on_tick(); }); } - - port_ = acceptor_.port(); - handler_->port(port_); - - CROW_LOG_INFO << server_name_ << " server is running at " << acceptor_.url_display(handler_->ssl_used()) << " using " << concurrency_ << " threads"; + handler_->port(acceptor_.local_endpoint().port()); + CROW_LOG_INFO << server_name_ + << " server is running at " << acceptor_.url_display(handler_->ssl_used()) + << " using " << concurrency_ << " threads"; CROW_LOG_INFO << "Call `app.loglevel(crow::LogLevel::Warning)` to hide Info level logs."; signals_.async_wait( @@ -190,16 +195,19 @@ namespace crow // NOTE: Already documented in "crow/app.h" io_context_.stop(); // Close main io_service } - uint16_t port(){ + uint16_t port() const { return acceptor_.local_endpoint().port(); } - /// Wait until the server has properly started - void wait_for_start() + /// Wait until the server has properly started or until timeout + std::cv_status wait_for_start(std::chrono::steady_clock::time_point wait_until) { std::unique_lock lock(start_mutex_); - while (!server_started_) - cv_started_.wait(lock); + + std::cv_status status = std::cv_status::no_timeout; + while (!server_started_ && ( status==std::cv_status::no_timeout )) + status = cv_started_.wait_until(lock,wait_until); + return status; } void signal_clear() @@ -288,8 +296,6 @@ namespace crow // NOTE: Already documented in "crow/app.h" uint16_t concurrency_{2}; std::uint8_t timeout_; std::string server_name_; - uint16_t port_; - std::string bindaddr_; bool use_unix_; std::vector> task_queue_length_pool_; diff --git a/include/crow/routing.h b/include/crow/routing.h index 50b47b26f..046241cf0 100644 --- a/include/crow/routing.h +++ b/include/crow/routing.h @@ -445,7 +445,7 @@ namespace crow // NOTE: Already documented in "crow/app.h" void handle_upgrade(const request& req, response&, SocketAdaptor&& adaptor) override { max_payload_ = max_payload_override_ ? max_payload_ : app_->websocket_max_payload(); - new crow::websocket::Connection(req, std::move(adaptor), app_, max_payload_, subprotocols_, open_handler_, message_handler_, close_handler_, error_handler_, accept_handler_); + new crow::websocket::Connection(req, std::move(adaptor), app_, max_payload_, subprotocols_, open_handler_, message_handler_, close_handler_, error_handler_, accept_handler_, mirror_protocols_); } void handle_upgrade(const request& req, response&, UnixSocketAdaptor&& adaptor) override { @@ -455,7 +455,7 @@ namespace crow // NOTE: Already documented in "crow/app.h" #ifdef CROW_ENABLE_SSL void handle_upgrade(const request& req, response&, SSLAdaptor&& adaptor) override { - new crow::websocket::Connection(req, std::move(adaptor), app_, max_payload_, subprotocols_, open_handler_, message_handler_, close_handler_, error_handler_, accept_handler_); + new crow::websocket::Connection(req, std::move(adaptor), app_, max_payload_, subprotocols_, open_handler_, message_handler_, close_handler_, error_handler_, accept_handler_, mirror_protocols_); } #endif @@ -508,6 +508,12 @@ namespace crow // NOTE: Already documented in "crow/app.h" return *this; } + self_t& mirrorprotocols(bool mirror_protocols = true) + { + mirror_protocols_ = mirror_protocols; + return *this; + } + protected: App* app_; std::function open_handler_; @@ -515,6 +521,7 @@ namespace crow // NOTE: Already documented in "crow/app.h" std::function close_handler_; std::function error_handler_; std::function accept_handler_; + bool mirror_protocols_ = false; uint64_t max_payload_; bool max_payload_override_ = false; std::vector subprotocols_; diff --git a/include/crow/websocket.h b/include/crow/websocket.h index 7198ad83f..9659a9de6 100644 --- a/include/crow/websocket.h +++ b/include/crow/websocket.h @@ -116,7 +116,8 @@ namespace crow // NOTE: Already documented in "crow/app.h" std::function message_handler, std::function close_handler, std::function error_handler, - std::function accept_handler): + std::function accept_handler, + bool mirror_protocols): adaptor_(std::move(adaptor)), handler_(handler), max_payload_bytes_(max_payload), @@ -145,6 +146,11 @@ namespace crow // NOTE: Already documented in "crow/app.h" } } + if (mirror_protocols & !requested_subprotocols_header.empty()) + { + subprotocol_ = requested_subprotocols_header; + } + if (accept_handler_) { void* ud = nullptr; diff --git a/tests/unittest.cpp b/tests/unittest.cpp index d294d0133..eeb9f35ac 100644 --- a/tests/unittest.cpp +++ b/tests/unittest.cpp @@ -2,6 +2,7 @@ #define CROW_LOG_LEVEL 0 #include +#include #include #include #include @@ -643,7 +644,22 @@ TEST_CASE("server_handling_error_request") app.stop(); } // server_handling_error_request -TEST_CASE("server_dynamic_port_allication") +TEST_CASE("server_invalid_ip_address") +{ + SimpleApp app; + CROW_ROUTE(app, "/") + ([] { + return "A"; + }); + auto _ = app.bindaddr("192.").port(45451).run_async(); + auto state = app.wait_for_server_start(); + + // we should run into a timeout as the server will not started + CHECK(state==cv_status::timeout); +} // server_invalid_ip_address + + +TEST_CASE("server_dynamic_port_allocation") { SimpleApp app; CROW_ROUTE(app, "/") @@ -659,7 +675,7 @@ TEST_CASE("server_dynamic_port_allication") asio::ip::make_address(LOCALHOST_ADDRESS), app.port())); } app.stop(); -} // server_dynamic_port_allication +} // server_dynamic_port_allocation TEST_CASE("server_handling_error_request_http_version") { @@ -1522,6 +1538,7 @@ TEST_CASE("middleware_simple") App app; TCPAcceptor::endpoint endpoint(asio::ip::address::from_string(LOCALHOST_ADDRESS), 45451); decltype(app)::server_t server(&app, endpoint); + CROW_ROUTE(app, "/") ([&](const crow::request& req) { app.get_context(req); @@ -3212,6 +3229,57 @@ TEST_CASE("websocket_subprotocols") app.stop(); } +TEST_CASE("mirror_websocket_subprotocols") +{ + static std::string http_message = "GET /ws HTTP/1.1\r\nConnection: keep-alive, Upgrade\r\nupgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Protocol: protocol1, protocol2\r\nSec-WebSocket-Version: 13\r\nHost: localhost\r\n\r\n"; + + websocket::connection* connection = nullptr; + bool connected{false}; + + SimpleApp app; + + CROW_WEBSOCKET_ROUTE(app, "/ws") + .mirrorprotocols() + .onaccept([&](const crow::request& req, void**) { + CROW_LOG_INFO << "Accepted websocket with URL " << req.url; + return true; + }) + .onopen([&](websocket::connection& con) { + connected = true; + connection = &con; + CROW_LOG_INFO << "Connected websocket and subprotocol is " << con.get_subprotocol(); + }) + .onclose([&](websocket::connection&, const std::string&, uint16_t) { + CROW_LOG_INFO << "Closing websocket"; + }); + + app.validate(); + + auto _ = app.bindaddr(LOCALHOST_ADDRESS).port(45451).run_async(); + app.wait_for_server_start(); + asio::io_context ic; + + asio::ip::tcp::socket c(ic); + c.connect(asio::ip::tcp::endpoint( + asio::ip::make_address(LOCALHOST_ADDRESS), 45451)); + + + char buf[2048]; + + //----------Handshake---------- + { + std::fill_n(buf, 2048, 0); + c.send(asio::buffer(http_message)); + + c.receive(asio::buffer(buf, 2048)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + CHECK(connected); + CHECK(connection->get_subprotocol() == "protocol1, protocol2"); + } + + app.stop(); +} + #ifdef CROW_ENABLE_COMPRESSION TEST_CASE("zlib_compression") { @@ -3862,13 +3930,26 @@ TEST_CASE("task_timer") b = true; }); - this_thread::sleep_for(3 * timer.get_tick_length()); + asio::steady_timer t(io_context); + asio_error_code ec; + + t.expires_from_now(3 * timer.get_tick_length()); + t.wait(ec); + // we are at 3 ticks, nothing be changed yet + CHECK(!ec); CHECK(a == false); CHECK(b == false); - this_thread::sleep_for(3 * timer.get_tick_length()); + t.expires_from_now(3 * timer.get_tick_length()); + t.wait(ec); + // we are at 3+3 = 6 ticks, so first task_timer handler should have runned + CHECK(!ec); CHECK(a == true); CHECK(b == false); - this_thread::sleep_for(5 * timer.get_tick_length()); + + t.expires_from_now(5 * timer.get_tick_length()); + t.wait(ec); + //we are at 3+3 +5 = 11 ticks, both task_timer handlers shoudl have run now + CHECK(!ec); CHECK(a == true); CHECK(b == true); @@ -3981,3 +4062,65 @@ TEST_CASE("unix_socket") } app.stop(); } // unix_socket + +TEST_CASE("option_header_passed_in_full") +{ + const std::string ServerName = "AN_EXTREMELY_UNIQUE_SERVER_NAME"; + + crow::App + app; + + app.get_middleware() // + .global() + .allow_credentials() + .expose("X-Total-Pages", "X-Total-Entries", "Content-Disposition"); + + app.server_name(ServerName); + + + CROW_ROUTE(app, "/echo").methods(crow::HTTPMethod::Options)([]() { + crow::response response{}; + response.add_header("Key", "Value"); + return response; + }); + + auto _ = app.bindaddr(LOCALHOST_ADDRESS).port(45451).run_async(); + + app.wait_for_server_start(); + + asio::io_service is; + + auto make_request = [&](const std::string& rq) { + asio::ip::tcp::socket c(is); + c.connect(asio::ip::tcp::endpoint( + asio::ip::address::from_string(LOCALHOST_ADDRESS), 45451)); + c.send(asio::buffer(rq)); + std::string fullString{}; + asio::error_code error; + char buffer[1024]; + while (true) + { + size_t len = c.read_some(asio::buffer(buffer), error); + + if (error == asio::error::eof) + { + break; + } + else if (error) + { + throw system_error(error); + } + + fullString.append(buffer, len); + } + c.close(); + return fullString; + }; + + std::string request = + "OPTIONS /echo HTTP/1.1\r\n"; + + auto res = make_request(request); + CHECK(res.find(ServerName) != std::string::npos); + app.stop(); +}