diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d64249..27d8e37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ project(curl_wrapper LANGUAGES CXX) find_package(CURL 7.52 REQUIRED) +find_package(Boost 1.54 REQUIRED COMPONENTS regex) add_subdirectory(src) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 423993c..5fd64c4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,5 +18,7 @@ else() target_link_libraries(${PROJECT_NAME} PUBLIC ${CURL_LIBRARIES}) endif() +target_link_libraries(${PROJECT_NAME} PRIVATE Boost::regex) + target_include_directories(${PROJECT_NAME} PUBLIC "$") diff --git a/src/curl_wrapper.cpp b/src/curl_wrapper.cpp index 050aed5..3e547ce 100644 --- a/src/curl_wrapper.cpp +++ b/src/curl_wrapper.cpp @@ -16,6 +16,8 @@ #include "curl_wrapper.hpp" +#include "types.hpp" + #include #include @@ -92,7 +94,7 @@ void CURLWrapper::set_useragent(string_view useragent) check(curl_easy_setopt(_connection, CURLOPT_USERAGENT, useragent.data())); } -string CURLWrapper::make_http_request(http_method method, string_view uri) +answer CURLWrapper::make_http_request(http_method method, string_view uri) { _buffer_headers.clear(); _buffer_body.clear(); @@ -171,10 +173,8 @@ string CURLWrapper::make_http_request(http_method method, string_view uri) long http_status{0}; // NOLINT(google-runtime-int) // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) check(curl_easy_getinfo(_connection, CURLINFO_RESPONSE_CODE, &http_status)); - // status code is in http_status, headers are in _buffer_headers, body is in - // _buffer_body. - // TODO: Communicate the above. - return _buffer_body; + return {static_cast(http_status), _buffer_headers, + _buffer_body}; } size_t CURLWrapper::writer_body(char *data, size_t size, size_t nmemb) diff --git a/src/curl_wrapper.hpp b/src/curl_wrapper.hpp index 0683805..1daf884 100644 --- a/src/curl_wrapper.hpp +++ b/src/curl_wrapper.hpp @@ -17,7 +17,9 @@ #ifndef CURL_WRAPPER_HPP #define CURL_WRAPPER_HPP -#include "curl/curl.h" +#include "types.hpp" + +#include #include #include @@ -29,16 +31,11 @@ namespace curl_wrapper using std::string; using std::string_view; -enum class http_method -{ - DELETE, - GET, - HEAD, - PATCH, - POST, - PUT -}; - +/*! + * @brief Light wrapper atound libcurl. + * + * @since INSERT_VERSION + */ class CURLWrapper { public: @@ -69,8 +66,9 @@ public: /*! * @brief Cleans up curl and connection. * - * May call `curl_global_cleanup`, which is not thread-safe. For more - * information consult [curl_global_cleanup(3)] + * Calls `curl_global_cleanup`, which is not thread-safe, when the last + * instance of CURLWrapper is destroyed. For more information consult + * [curl_global_cleanup(3)] * (https://curl.haxx.se/libcurl/c/curl_global_cleanup.html). * * @since INSERT_VERSION @@ -92,7 +90,7 @@ public: * * @since INSERT_VERSION */ - inline CURL *get_curl_easy_handle() + [[nodiscard]] inline CURL *get_curl_easy_handle() const { return _connection; } @@ -156,11 +154,11 @@ public: * @param method The HTTP method. * @param uri The full URI. * - * @return The body of the page. + * @return The status code, headers and body of the page. * * @since INSERT_VERSION */ - [[nodiscard]] string make_http_request(http_method method, string_view uri); + [[nodiscard]] answer make_http_request(http_method method, string_view uri); private: CURL *_connection; diff --git a/src/types.cpp b/src/types.cpp new file mode 100644 index 0000000..af89cfd --- /dev/null +++ b/src/types.cpp @@ -0,0 +1,43 @@ +/* This file is part of curl_wrapper. + * Copyright © 2020 tastytea + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include "types.hpp" + +#include + +#include +#include + +namespace curl_wrapper +{ + +std::string answer::get_header(const std::string_view field) const +{ + const boost::regex re_field(string("^") + field.data() + + R"(:\s+([^\r\n]+))", + boost::regex::icase); + boost::cmatch match; + + boost::regex_search(headers.c_str(), match, re_field); + if (match[1].matched) + { + return match[1].str(); + } + + return {}; +} + +} // namespace curl_wrapper diff --git a/src/types.hpp b/src/types.hpp new file mode 100644 index 0000000..66fd0b6 --- /dev/null +++ b/src/types.hpp @@ -0,0 +1,105 @@ +/* This file is part of curl_wrapper. + * Copyright © 2020 tastytea + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#ifndef CURL_WRAPPER_TYPES_HPP +#define CURL_WRAPPER_TYPES_HPP + +#include +#include +#include +#include + +namespace curl_wrapper +{ + +using std::ostream; +using std::string; +using std::string_view; + +/*! + * @brief The HTTP method. + * + * @since INSERT_VERSION + */ +enum class http_method +{ + DELETE, + GET, + HEAD, + PATCH, + POST, + PUT +}; + +/*! + * @brief Return type for network requests. + * + * Currently only HTTP is considered. + * + * @since INSERT_VERSION + */ +struct answer +{ + std::uint16_t status{0}; + string headers; + string body; + + /*! + * @brief Returns true if #status is 200. + * + * @since INSERT_VERSION + */ + [[nodiscard]] inline explicit operator bool() const + { + return (status == 200); + } + + /*! + * @brief Returns #body as const std::string_view. + * + * @since INSERT_VERSION + */ + [[nodiscard]] inline explicit operator string_view() const + { + return body; + } + + /*! + * @brief Returns #body as std::ostream. + * + * @since INSERT_VERSION + */ + inline friend ostream &operator<<(ostream &out, const answer &answer) + { + out << answer.body; + return out; + } + + /*! + * @brief Returns the value of a header field. + * + * @param field Case insensitive, uses default locale. + * + * @return The value of the header field as std::string or {} if not found. + * + * @since INSERT_VERSION + */ + [[nodiscard]] string get_header(string_view field) const; +}; + +} // namespace curl_wrapper + +#endif // CURL_WRAPPER_TYPES_HPP diff --git a/tests/test_get.cpp b/tests/test_get.cpp index 48e5e97..ffacf4d 100644 --- a/tests/test_get.cpp +++ b/tests/test_get.cpp @@ -22,7 +22,7 @@ SCENARIO("HTTP GET", "[http]") try { CURLWrapper curl; - answer = curl.make_http_request(http_method::GET, uri); + answer = curl.make_http_request(http_method::GET, uri).body; } catch (const std::exception &e) { diff --git a/tests/test_headers.cpp b/tests/test_headers.cpp new file mode 100644 index 0000000..456ec78 --- /dev/null +++ b/tests/test_headers.cpp @@ -0,0 +1,51 @@ +#include "types.hpp" + +#include + +#include +#include + +namespace curl_wrapper +{ + +using std::string; + +SCENARIO("Extract header") +{ + answer ret; + ret.headers = R"(HTTP/1.1 200 OK +Server: nginx/1.18.0 +Date: Sat, 07 Nov 2020 22:26:13 GMT +Content-Type: application/rss+xml; charset=utf-8 +Connection: keep-alive +Keep-Alive: timeout=20 +Expires: Sat, 07 Nov 2020 22:56:13 GMT +Cache-Control: max-age=1800 +X-Cache: HIT +)"; + const string searchfor{"cache-control"}; + + bool exception = false; + string value; + + WHEN("We search for " + searchfor) + { + try + { + value = ret.get_header(searchfor); + } + catch (const std::exception &e) + { + exception = true; + } + + THEN("No exception is thrown") + AND_THEN("The value is successfully extracted") + { + REQUIRE_FALSE(exception); + REQUIRE(value == "max-age=1800"); + } + } +} + +} // namespace curl_wrapper