diff --git a/include/cinatra/coro_http_client.hpp b/include/cinatra/coro_http_client.hpp index f4bfa0cf..8fc1f10f 100644 --- a/include/cinatra/coro_http_client.hpp +++ b/include/cinatra/coro_http_client.hpp @@ -144,11 +144,11 @@ class coro_http_client : public std::enable_shared_from_this { }; coro_http_client(asio::io_context::executor_type executor) - : socket_(std::make_shared(executor)), + : executor_wrapper_(executor), + timer_(&executor_wrapper_), + socket_(std::make_shared(executor)), read_buf_(socket_->read_buf_), - chunked_buf_(socket_->chunked_buf_), - executor_wrapper_(executor), - timer_(&executor_wrapper_) {} + chunked_buf_(socket_->chunked_buf_) {} coro_http_client( coro_io::ExecutorWrapper<> *executor = coro_io::get_global_executor()) @@ -644,7 +644,7 @@ class coro_http_client : public std::enable_shared_from_this { } std::error_code err_code; timer_.cancel(err_code); - auto ret = co_await std::move(future); + co_await std::move(future); if (is_timeout_) { co_return std::make_error_code(std::errc::timed_out); } @@ -752,6 +752,7 @@ class coro_http_client : public std::enable_shared_from_this { req_context<> ctx{}; if (range.empty()) { + add_header("Transfer-Encoding", "chunked"); ctx = {req_content_type::none, "", "", std::move(file)}; } else { diff --git a/include/cinatra/coro_http_connection.hpp b/include/cinatra/coro_http_connection.hpp index 5776cf31..346aa3b9 100644 --- a/include/cinatra/coro_http_connection.hpp +++ b/include/cinatra/coro_http_connection.hpp @@ -88,7 +88,9 @@ class coro_http_connection #endif async_simple::coro::Lazy start() { +#ifdef CINATRA_ENABLE_SSL bool has_shake = false; +#endif while (true) { #ifdef CINATRA_ENABLE_SSL if (use_ssl_ && !has_shake) { @@ -220,6 +222,45 @@ class coro_http_connection co_return true; } + async_simple::coro::Lazy write_data(std::string_view message) { + std::vector buffers; + buffers.push_back(asio::buffer(message)); + auto [ec, _] = co_await async_write(buffers); + if (ec) { + CINATRA_LOG_ERROR << "async_write error: " << ec.message(); + close(); + co_return false; + } + + if (!keep_alive_) { + // now in io thread, so can close socket immediately. + close(); + } + + co_return true; + } + + async_simple::coro::Lazy write_chunked_data(std::string_view buf, + bool eof) { + std::string chunk_size_str = ""; + std::vector buffers = + to_chunked_buffers(buf.data(), buf.length(), + chunk_size_str, eof); + auto [ec, _] = co_await async_write(std::move(buffers)); + if (ec) { + CINATRA_LOG_ERROR << "async_write error: " << ec.message(); + close(); + co_return false; + } + + if (!keep_alive_) { + // now in io thread, so can close socket immediately. + close(); + } + + co_return true; + } + bool sync_reply() { return async_simple::coro::syncAwait(reply()); } async_simple::coro::Lazy begin_chunked() { diff --git a/include/cinatra/coro_http_request.hpp b/include/cinatra/coro_http_request.hpp index 3dfafe5e..7e3b4e60 100644 --- a/include/cinatra/coro_http_request.hpp +++ b/include/cinatra/coro_http_request.hpp @@ -54,6 +54,8 @@ class coro_http_request { return is_chunk; } + bool is_ranges() { return parser_.is_ranges(); } + content_type get_content_type() { static content_type thread_local content_type = get_content_type_impl(); return content_type; diff --git a/include/cinatra/coro_http_server.hpp b/include/cinatra/coro_http_server.hpp index 0a71ec28..3ba84edf 100644 --- a/include/cinatra/coro_http_server.hpp +++ b/include/cinatra/coro_http_server.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -10,6 +11,8 @@ #include "async_simple/coro/Lazy.h" #include "cinatra/coro_http_response.hpp" #include "cinatra/coro_http_router.hpp" +#include "cinatra/mime_types.hpp" +#include "cinatra/utils.hpp" #include "cinatra_log_wrapper.hpp" #include "coro_http_connection.hpp" #include "ylt/coro_io/coro_io.hpp" @@ -78,7 +81,7 @@ class coro_http_server { promise.setValue(ec); } - return std::move(future); + return future; } // only call once, not thread safe. @@ -150,6 +153,139 @@ class coro_http_server { } } + void set_transfer_chunked_size(size_t size) { chunked_size_ = size; } + + void set_static_res_handler(std::string_view uri_suffix = "", + std::string file_path = "www") { + bool has_double_dot = (file_path.find("..") != std::string::npos) || + (uri_suffix.find("..") != std::string::npos); + if (std::filesystem::path(file_path).has_root_path() || + std::filesystem::path(uri_suffix).has_root_path() || has_double_dot) { + CINATRA_LOG_ERROR << "invalid file path: " << file_path; + std::exit(1); + } + + if (!uri_suffix.empty()) { + static_dir_router_path_ = + std::filesystem::path(uri_suffix).make_preferred().string(); + } + + if (!file_path.empty()) { + static_dir_ = std::filesystem::path(file_path).make_preferred().string(); + } + else { + static_dir_ = fs::absolute(fs::current_path().string()).string(); + } + + files_.clear(); + for (const auto &file : + std::filesystem::recursive_directory_iterator(static_dir_)) { + if (!file.is_directory()) { + files_.push_back(file.path().string()); + } + } + + std::filesystem::path router_path = + std::filesystem::path(static_dir_router_path_); + + std::string uri; + for (auto &file : files_) { + auto relative_path = + std::filesystem::path(file.substr(static_dir_.length())).string(); + if (size_t pos = relative_path.find('\\') != std::string::npos) { + replace_all(relative_path, "\\", "/"); + } + uri = std::string("/") + .append(static_dir_router_path_) + .append(relative_path); + + set_http_handler( + uri, + [this, file_name = file]( + coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + bool is_chunked = req.is_chunked(); + bool is_ranges = req.is_ranges(); + if (!is_chunked && !is_ranges) { + resp.set_status(status_type::not_implemented); + co_return; + } + + std::string_view extension = get_extension(file_name); + std::string_view mime = get_mime_type(extension); + + std::string content; + detail::resize(content, chunked_size_); + + coro_io::coro_file in_file{}; + co_await in_file.async_open(file_name, coro_io::flags::read_only); + if (!in_file.is_open()) { + resp.set_status_and_content(status_type::not_found, + file_name + "not found"); + co_return; + } + + if (is_chunked) { + resp.set_format_type(format_type::chunked); + bool ok; + if (ok = co_await resp.get_conn()->begin_chunked(); !ok) { + co_return; + } + + while (true) { + auto [ec, size] = + co_await in_file.async_read(content.data(), content.size()); + if (ec) { + resp.set_status(status_type::no_content); + co_await resp.get_conn()->reply(); + co_return; + } + + bool r = co_await resp.get_conn()->write_chunked( + std::string_view(content.data(), size)); + if (!r) { + co_return; + } + + if (in_file.eof()) { + co_await resp.get_conn()->end_chunked(); + break; + } + } + } + else if (is_ranges) { + auto range_header = build_range_header( + mime, file_name, coro_io::coro_file::file_size(file_name)); + resp.set_delay(true); + bool r = co_await req.get_conn()->write_data(range_header); + if (!r) { + co_return; + } + + while (true) { + auto [ec, size] = + co_await in_file.async_read(content.data(), content.size()); + if (ec) { + resp.set_status(status_type::no_content); + co_await resp.get_conn()->reply(); + co_return; + } + + r = co_await req.get_conn()->write_data( + std::string_view(content.data(), size)); + if (!r) { + co_return; + } + + if (in_file.eof()) { + break; + } + } + } + }); + } + } + void set_check_duration(auto duration) { check_duration_ = duration; } void set_timeout_duration( @@ -305,6 +441,20 @@ class coro_http_server { } } + std::string build_range_header(std::string_view mime, + std::string_view filename, size_t file_size) { + std::string header_str = + "HTTP/1.1 200 OK\r\nAccess-Control-Allow-origin: " + "*\r\nAccept-Ranges: bytes\r\n"; + header_str.append("Content-Disposition: attachment;filename="); + header_str.append(filename).append("\r\n"); + header_str.append("Connection: keep-alive\r\n"); + header_str.append("Content-Type: ").append(mime).append("\r\n"); + header_str.append("Content-Length: "); + header_str.append(std::to_string(file_size)).append("\r\n\r\n"); + return header_str; + } + private: std::unique_ptr pool_; asio::io_context *out_ctx_ = nullptr; @@ -325,6 +475,11 @@ class coro_http_server { asio::steady_timer check_timer_; bool need_check_ = false; std::atomic stop_timer_ = false; + + std::string static_dir_router_path_ = ""; + std::string static_dir_ = ""; + std::vector files_; + size_t chunked_size_ = 1024 * 10; #ifdef CINATRA_ENABLE_SSL std::string cert_file_; std::string key_file_; diff --git a/include/cinatra/http_parser.hpp b/include/cinatra/http_parser.hpp index 308d07ca..2935f10e 100644 --- a/include/cinatra/http_parser.hpp +++ b/include/cinatra/http_parser.hpp @@ -127,7 +127,7 @@ class http_parser { } bool is_ranges() const { - auto transfer_encoding = this->get_header_value("Accept-Ranges"sv); + auto transfer_encoding = this->get_header_value("Range"sv); return !transfer_encoding.empty(); } diff --git a/include/cinatra/uri.hpp b/include/cinatra/uri.hpp index 39750404..40943256 100644 --- a/include/cinatra/uri.hpp +++ b/include/cinatra/uri.hpp @@ -308,7 +308,7 @@ struct context { port(u.get_port()), path(u.get_path()), query(u.get_query()), - method(mthd), - body(std::move(b)) {} + body(std::move(b)), + method(mthd) {} }; } // namespace cinatra \ No newline at end of file diff --git a/include/cinatra/websocket.hpp b/include/cinatra/websocket.hpp index 52c9a1e7..bb0509e2 100644 --- a/include/cinatra/websocket.hpp +++ b/include/cinatra/websocket.hpp @@ -304,7 +304,6 @@ class websocket { std::string_view sec_ws_key_; size_t payload_length_ = 0; - size_t left_payload_length_ = 0; size_t left_header_len_ = 0; uint8_t mask_[4] = {}; diff --git a/tests/test_coro_http_server.cpp b/tests/test_coro_http_server.cpp index c671e1cf..78029589 100644 --- a/tests/test_coro_http_server.cpp +++ b/tests/test_coro_http_server.cpp @@ -766,7 +766,6 @@ TEST_CASE("test websocket with different message sizes") { return; } - size_t size = data.resp_body.size(); std::cout << "ws msg len: " << data.resp_body.size() << std::endl; REQUIRE(data.resp_body == medium_message); }); @@ -861,7 +860,6 @@ TEST_CASE("test websocket with message max_size limit") { return; } - size_t size = data.resp_body.size(); std::cout << "ws msg len: " << data.resp_body.size() << std::endl; REQUIRE(data.resp_body == medium_message); }); @@ -922,6 +920,48 @@ TEST_CASE("test ssl server") { } #endif +TEST_CASE("test http download server") { + cinatra::coro_http_server server(1, 9001); + std::string filename = "test_download.txt"; + create_file(filename, 1010); + + // curl http://127.0.0.1:9001/download/test_download.txt will download + // test_download.txt file + server.set_transfer_chunked_size(100); + server.set_static_res_handler("download", ""); + server.async_start(); + std::this_thread::sleep_for(200ms); + + { + coro_http_client client{}; + auto result = async_simple::coro::syncAwait(client.async_download( + "http://127.0.0.1:9001/download/test_download.txt", "download.txt")); + + CHECK(result.status == 200); + std::string download_file = fs::absolute("download.txt").string(); + std::ifstream ifs(download_file, std::ios::binary); + std::string content((std::istreambuf_iterator(ifs)), + (std::istreambuf_iterator())); + CHECK(content.size() == 1010); + CHECK(content[0] == 'A'); + } + + { + coro_http_client client{}; + auto result = async_simple::coro::syncAwait(client.async_download( + "http://127.0.0.1:9001/download/test_download.txt", "download.txt", + "0-")); + + CHECK(result.status == 200); + std::string download_file = fs::absolute("download.txt").string(); + std::ifstream ifs(download_file, std::ios::binary); + std::string content((std::istreambuf_iterator(ifs)), + (std::istreambuf_iterator())); + CHECK(content.size() == 1010); + CHECK(content[0] == 'A'); + } +} + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) int main(int argc, char **argv) { return doctest::Context(argc, argv).run(); } DOCTEST_MSVC_SUPPRESS_WARNING_POP \ No newline at end of file